Coverage for src\derivepassphrase\cli.py: 100.000%
466 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2#
3# SPDX-License-Identifier: Zlib
5# ruff: noqa: TRY400
7"""Command-line interface for derivepassphrase."""
9from __future__ import annotations
11import base64
12import collections
13import contextlib
14import json
15import logging
16import os
17from typing import (
18 TYPE_CHECKING,
19 Final,
20 Literal,
21 NoReturn,
22 TextIO,
23 cast,
24)
26import click
27import click.shell_completion
28from typing_extensions import (
29 Any,
30)
32from derivepassphrase import _internals, _types, exporter, vault
33from derivepassphrase._internals import cli_helpers, cli_machinery
34from derivepassphrase._internals import cli_messages as _msg
36if TYPE_CHECKING:
37 from collections.abc import Sequence
38 from collections.abc import Set as AbstractSet
39 from contextlib import AbstractContextManager
41__all__ = ('derivepassphrase',)
43PROG_NAME = _internals.PROG_NAME
44VERSION = _internals.VERSION
47@click.group(
48 context_settings={
49 'help_option_names': ['-h', '--help'],
50 'ignore_unknown_options': True,
51 'allow_interspersed_args': False,
52 },
53 epilog=_msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EPILOG_01),
54 invoke_without_command=True,
55 cls=cli_machinery.TopLevelCLIEntryPoint,
56 help=(
57 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_01),
58 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_02),
59 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_03),
60 ),
61)
62@cli_machinery.version_option(
63 cli_machinery.derivepassphrase_version_option_callback
64)
65@cli_machinery.color_forcing_pseudo_option
66@cli_machinery.standard_logging_options
67@click.pass_context
68def derivepassphrase(ctx: click.Context, /) -> None:
69 """Derive a strong passphrase, deterministically, from a master secret.
71 This is a [`click`][CLICK]-powered command-line interface function,
72 and not intended for programmatic use. See the derivepassphrase(1)
73 manpage for full documentation of the interface. (See also
74 [`click.testing.CliRunner`][] for controlled, programmatic
75 invocation.)
77 [CLICK]: https://pypi.org/package/click/
79 """
80 # TODO(the-13th-letter): Turn this callback into a no-op in v1.0.
81 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands
82 deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') 1:-;.q=/?W9+wO
83 if ctx.invoked_subcommand is None: 1:-;.q=/?W9+wO
84 deprecation.warning( 1O
85 _msg.TranslatedString(
86 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED
87 ),
88 extra={'color': ctx.color},
89 )
90 # See definition of click.Group.invoke, non-chained case.
91 with ctx: 1O
92 sub_ctx = derivepassphrase_vault.make_context( 1O
93 'vault', ctx.args, parent=ctx
94 )
95 with sub_ctx: 1O
96 return derivepassphrase_vault.invoke(sub_ctx) 1O
97 return None 1:-;.q=/?W9+w
100# Exporter
101# ========
104@derivepassphrase.group(
105 'export',
106 context_settings={
107 'help_option_names': ['-h', '--help'],
108 'ignore_unknown_options': True,
109 'allow_interspersed_args': False,
110 },
111 invoke_without_command=True,
112 cls=cli_machinery.DefaultToVaultGroup,
113 help=(
114 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_01),
115 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_02),
116 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_03),
117 ),
118)
119@cli_machinery.version_option(cli_machinery.export_version_option_callback)
120@cli_machinery.color_forcing_pseudo_option
121@cli_machinery.standard_logging_options
122@click.pass_context
123def derivepassphrase_export(ctx: click.Context, /) -> None:
124 """Export a foreign configuration to standard output.
126 This is a [`click`][CLICK]-powered command-line interface function,
127 and not intended for programmatic use. See the
128 derivepassphrase-export(1) manpage for full documentation of the
129 interface. (See also [`click.testing.CliRunner`][] for controlled,
130 programmatic invocation.)
132 [CLICK]: https://pypi.org/package/click/
134 """
135 # TODO(the-13th-letter): Turn this callback into a no-op in v1.0.
136 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands
137 deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') 1-./9+
138 if ctx.invoked_subcommand is None: 1-./9+
139 deprecation.warning( 1+
140 _msg.TranslatedString(
141 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED
142 ),
143 extra={'color': ctx.color},
144 )
145 # See definition of click.Group.invoke, non-chained case.
146 with ctx: 1+
147 sub_ctx = derivepassphrase_export_vault.make_context( 1+
148 'vault', ctx.args, parent=ctx
149 )
150 # Constructing the subcontext above will usually already
151 # lead to a click.UsageError, so this block typically won't
152 # actually be called.
153 with sub_ctx: # pragma: no cover
154 return derivepassphrase_export_vault.invoke(sub_ctx) 1+
155 return None 1-./9
158@derivepassphrase_export.command(
159 'vault',
160 context_settings={'help_option_names': ['-h', '--help']},
161 cls=cli_machinery.CommandWithHelpGroups,
162 help=(
163 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_01),
164 _msg.TranslatedString(
165 _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_02,
166 path_metavar=_msg.TranslatedString(
167 _msg.Label.EXPORT_VAULT_METAVAR_PATH,
168 ),
169 ),
170 _msg.TranslatedString(
171 _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_03,
172 path_metavar=_msg.TranslatedString(
173 _msg.Label.EXPORT_VAULT_METAVAR_PATH,
174 ),
175 ),
176 ),
177)
178@click.option(
179 '-f',
180 '--format',
181 'formats',
182 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_FORMAT_METAVAR_FMT),
183 multiple=True,
184 default=('v0.3', 'v0.2', 'storeroom'),
185 type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
186 help=_msg.TranslatedString(
187 _msg.Label.EXPORT_VAULT_FORMAT_HELP_TEXT,
188 defaults_hint=_msg.TranslatedString(
189 _msg.Label.EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT,
190 ),
191 metavar=_msg.TranslatedString(
192 _msg.Label.EXPORT_VAULT_FORMAT_METAVAR_FMT,
193 ),
194 ),
195 cls=cli_machinery.StandardOption,
196)
197@click.option(
198 '-k',
199 '--key',
200 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_KEY_METAVAR_K),
201 help=_msg.TranslatedString(
202 _msg.Label.EXPORT_VAULT_KEY_HELP_TEXT,
203 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_KEY_METAVAR_K),
204 defaults_hint=_msg.TranslatedString(
205 _msg.Label.EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT,
206 ),
207 ),
208 cls=cli_machinery.StandardOption,
209)
210@cli_machinery.version_option(
211 cli_machinery.export_vault_version_option_callback
212)
213@cli_machinery.color_forcing_pseudo_option
214@cli_machinery.standard_logging_options
215@click.argument(
216 'path',
217 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_METAVAR_PATH),
218 required=True,
219 shell_complete=cli_helpers.shell_complete_path,
220)
221@click.pass_context
222def derivepassphrase_export_vault(
223 ctx: click.Context,
224 /,
225 *,
226 path: str | bytes | os.PathLike[str] | None,
227 formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
228 key: str | bytes | None = None,
229) -> None:
230 """Export a vault-native configuration to standard output.
232 This is a [`click`][CLICK]-powered command-line interface function,
233 and not intended for programmatic use. See the
234 derivepassphrase-export-vault(1) manpage for full documentation of
235 the interface. (See also [`click.testing.CliRunner`][] for
236 controlled, programmatic invocation.)
238 [CLICK]: https://pypi.org/package/click/
240 """
241 logger = logging.getLogger(PROG_NAME) 19'(#)$*%,!
242 if path in {'VAULT_PATH', b'VAULT_PATH'}: 19'(#)$*%,!
243 path = None 19'#!
244 if isinstance(key, str): # pragma: no branch 19'(#)$*%,!
245 key = key.encode('utf-8') 1(#
246 for fmt in formats: 19'(#)$*%,!
247 try: 19'(#)$*%,!
248 config = exporter.export_vault_config_data(path, key, format=fmt) 19'(#)$*%,!
249 except ( 1)$*%!
250 IsADirectoryError,
251 NotADirectoryError,
252 exporter.NotAVaultConfigError,
253 RuntimeError,
254 ):
255 logger.info( 1$%
256 _msg.TranslatedString(
257 _msg.InfoMsgTemplate.CANNOT_LOAD_AS_VAULT_CONFIG,
258 path=path or exporter.get_vault_path(),
259 fmt=fmt,
260 ),
261 extra={'color': ctx.color},
262 )
263 continue 1$%
264 except OSError as exc: 1)*!
265 logger.error( 1)*
266 _msg.TranslatedString(
267 _msg.ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG_OSERROR,
268 path=path,
269 error=exc.strerror,
270 filename=exc.filename,
271 ).maybe_without_filename(),
272 extra={'color': ctx.color},
273 )
274 ctx.exit(1) 1)*
275 except ModuleNotFoundError: 1!
276 logger.error( 1!
277 _msg.TranslatedString(
278 _msg.ErrMsgTemplate.MISSING_MODULE,
279 module='cryptography',
280 ),
281 extra={'color': ctx.color},
282 )
283 logger.info( 1!
284 _msg.TranslatedString(
285 _msg.InfoMsgTemplate.PIP_INSTALL_EXTRA,
286 extra_name='export',
287 ),
288 extra={'color': ctx.color},
289 )
290 ctx.exit(1) 1!
291 else:
292 if not _types.is_vault_config(config): 19'(#,
293 logger.error( 1,
294 _msg.TranslatedString(
295 _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG,
296 config=config,
297 ),
298 extra={'color': ctx.color},
299 )
300 ctx.exit(1) 1,
301 click.echo( 19'(#
302 json.dumps(
303 config, ensure_ascii=False, indent=2, sort_keys=True
304 ),
305 color=ctx.color,
306 )
307 break 19'(#
308 else:
309 logger.error( 1$%
310 _msg.TranslatedString(
311 _msg.ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG,
312 path=path,
313 ).maybe_without_filename(),
314 extra={'color': ctx.color},
315 )
316 ctx.exit(1) 1$%
319class _VaultContext: # noqa: PLR0904
320 """The context for the "vault" command-line interface.
322 This context object -- wrapping a [`click.Context`][] object --
323 encapsulates a single call to the `derivepassphrase vault`
324 command-line. Although fully documented, this class is an
325 implementation detail of the `derivepassphrase vault` command-line
326 and should not be instantiated directly by users or API clients.
328 Attributes:
329 logger:
330 The logger used for warnings and error messages.
331 deprecation:
332 The logger used for deprecation warnings.
333 ctx:
334 The underlying [`click.Context`][] from which the
335 command-line settings and parameter values are queried.
336 all_ops:
337 A list of operations supported by the CLI, in the
338 order that they are queried in the `click` context. The
339 final entry is the default operation, and does not
340 correspond to a `click` parameter; all others are `click`
341 parameter names.
342 readwrite_ops:
343 A set of operations which modify the `derivepassphrase`
344 state. All other operations are read-only.
345 options_in_group:
346 A mapping of option group names to lists of known options
347 from this group. Used during the validation of the command
348 line.
349 params_by_str:
350 A mapping of option names (long names, short names, etc.) to
351 option objects. Used during the validation of the command
352 line.
354 """
356 logger: Final = logging.getLogger(PROG_NAME)
357 """"""
358 deprecation: Final = logging.getLogger(PROG_NAME + '.deprecation')
359 """"""
360 ctx: Final[click.Context]
361 """"""
362 all_ops: tuple[str, ...] = (
363 'delete_service_settings',
364 'delete_globals',
365 'clear_all_settings',
366 'import_settings',
367 'export_settings',
368 'store_config_only',
369 # The default op "derive_passphrase" must be last!
370 'derive_passphrase',
371 )
372 readwrite_ops: AbstractSet[str] = frozenset({
373 'delete_service_settings',
374 'delete_globals',
375 'clear_all_settings',
376 'import_settings',
377 'store_config_only',
378 })
379 options_in_group: dict[type[click.Option], list[click.Option]]
380 params_by_str: dict[str, click.Parameter]
382 def __init__(self, ctx: click.Context, /) -> None:
383 """Initialize the vault context.
385 Args:
386 ctx:
387 The underlying [`click.Context`][] from which the
388 command-line settings and parameter values are queried.
390 """ # noqa: DOC501
391 self.ctx = ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
392 self.params_by_str = {} 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
393 self.options_in_group = {} 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
394 for param in ctx.command.params: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
395 if isinstance(param, click.Option): 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
396 group: type[click.Option]
397 known_option_groups = [ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
398 cli_machinery.PassphraseGenerationOption,
399 cli_machinery.ConfigurationOption,
400 cli_machinery.StorageManagementOption,
401 cli_machinery.LoggingOption,
402 cli_machinery.CompatibilityOption,
403 cli_machinery.StandardOption,
404 ]
405 if isinstance(param, cli_machinery.OptionGroupOption): 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
406 for class_ in known_option_groups: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
407 if isinstance(param, class_): 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
408 group = class_ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
409 break 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
410 else:
411 raise AssertionError( # noqa: TRY003
412 f'Unknown option group for {param!r}' # noqa: EM102
413 )
414 else:
415 group = click.Option 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
416 self.options_in_group.setdefault(group, []).append(param) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
417 self.params_by_str[param.human_readable_name] = param 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
418 for name in param.opts + param.secondary_opts: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
419 self.params_by_str[name] = param 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
421 def is_param_set(self, param: click.Parameter, /) -> bool:
422 """Return true if the parameter is set."""
423 return bool(self.ctx.params.get(param.human_readable_name)) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
425 def option_name(self, param: click.Parameter | str, /) -> str:
426 """Return the option name of a parameter.
428 Annoyingly, `param.human_readable_name` contains the *function*
429 parameter name, not the list of option names. *Those* are
430 stashed in the `.opts` and `.secondary_opts` attributes, which
431 are visible in the `.to_info_dict()` output, but not otherwise
432 documented. We return the shortest one among the long-form
433 option names.
435 Args:
436 param: The parameter whose option name is requested.
438 Raises:
439 ValueError: The parameter has no long-form option names.
441 """
442 param = self.params_by_str[param] if isinstance(param, str) else param 18
443 names = [param.human_readable_name, *param.opts, *param.secondary_opts] 18
444 option_names = [n for n in names if n.startswith('--')] 18
445 return min(option_names, key=len) 18
447 def check_incompatible_options(
448 self,
449 param1: click.Parameter | str,
450 param2: click.Parameter | str,
451 ) -> None:
452 """Raise an error if the two options are incompatible.
454 Raises:
455 click.BadOptionUsage: The given options are incompatible.
457 """
458 param1 = ( 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
459 self.params_by_str[param1] if isinstance(param1, str) else param1
460 )
461 param2 = ( 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
462 self.params_by_str[param2] if isinstance(param2, str) else param2
463 )
464 if param1 == param2: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
465 return 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
466 if not self.is_param_set(param1): 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
467 return 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
468 if self.is_param_set(param2): 1adqABpbf8zYZ17W360ntDPxuKQRSsmlcTr24JwUV5ghEeyvikjoC
469 param1_str = self.option_name(param1) 18
470 param2_str = self.option_name(param2) 18
471 raise click.BadOptionUsage( 18
472 param1_str,
473 str(
474 _msg.TranslatedString(
475 _msg.ErrMsgTemplate.PARAMS_MUTUALLY_EXCLUSIVE,
476 param1=param1_str,
477 param2=param2_str,
478 )
479 ),
480 ctx=self.ctx,
481 )
482 return 1adqABpbf8zYZ17W360ntDPxuKQRSsmlcTr24JwUV5ghEeyvikjoC
484 def err(self, msg: Any, /, **kwargs: Any) -> NoReturn: # noqa: ANN401
485 """Log an error, then abort the function call.
487 We ensure that color handling is done properly before the error
488 is logged.
490 """
491 stacklevel = kwargs.pop('stacklevel', 1) 1YZ1360nDPxuQRSmlTr24Jko
492 stacklevel += 1 1YZ1360nDPxuQRSmlTr24Jko
493 extra = kwargs.pop('extra', {}) 1YZ1360nDPxuQRSmlTr24Jko
494 extra.setdefault('color', self.ctx.color) 1YZ1360nDPxuQRSmlTr24Jko
495 self.logger.error(msg, stacklevel=stacklevel, extra=extra, **kwargs) 1YZ1360nDPxuQRSmlTr24Jko
496 self.ctx.exit(1) 1YZ1360nDPxuQRSmlTr24Jko
498 def warning(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401
499 """Log a warning.
501 We ensure that color handling is done properly before the
502 warning is logged.
504 """
505 stacklevel = kwargs.pop('stacklevel', 1) 1qpfzJVghEeyvijC
506 stacklevel += 1 1qpfzJVghEeyvijC
507 extra = kwargs.pop('extra', {}) 1qpfzJVghEeyvijC
508 extra.setdefault('color', self.ctx.color) 1qpfzJVghEeyvijC
509 self.logger.warning(msg, stacklevel=stacklevel, extra=extra, **kwargs) 1qpfzJVghEeyvijC
511 def deprecation_warning(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401
512 """Log a deprecation warning.
514 We ensure that color handling is done properly before the
515 warning is logged.
517 """
518 stacklevel = kwargs.pop('stacklevel', 1) 1UV
519 stacklevel += 1 1UV
520 extra = kwargs.pop('extra', {}) 1UV
521 extra.setdefault('color', self.ctx.color) 1UV
522 self.deprecation.warning( 1UV
523 msg, stacklevel=stacklevel, extra=extra, **kwargs
524 )
526 def deprecation_info(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401
527 """Log a deprecation info message.
529 We ensure that color handling is done properly before the
530 warning is logged.
532 """
533 stacklevel = kwargs.pop('stacklevel', 1) 1U
534 stacklevel += 1 1U
535 extra = kwargs.pop('extra', {}) 1U
536 extra.setdefault('color', self.ctx.color) 1U
537 self.deprecation.info( 1U
538 msg, stacklevel=stacklevel, extra=extra, **kwargs
539 )
541 def get_config(self) -> _types.VaultConfig:
542 """Return the vault configuration stored on disk.
544 If no configuration is stored, return an empty configuration.
546 If only a v0.1-style configuration is found, attempt to migrate
547 it. This will be removed in v1.0.
549 """
550 try: 1adqABHLMIpbfz7W360ntDPxuKQRSXNsmlcrJwOUV5ghEeyGvFikjoC
551 return cli_helpers.load_config() 1adqABHLMIpbfz7W360ntDPxuKQRSXNsmlcrJwOUV5ghEeyGvFikjoC
552 except FileNotFoundError: 1qABW360XNsmlwOUV
553 # TODO(the-13th-letter): Return the empty default
554 # configuration directly.
555 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes.html#v1.0-old-settings-file
556 try: 1qABW0XNsmlwOUV
557 backup_config, exc = cli_helpers.migrate_and_load_old_config() 1qABW0XNsmlwOUV
558 except FileNotFoundError: 1qABW0XNsmlwO
559 return {'services': {}} 1qABW0XNsmlwO
560 old_name = cli_helpers.config_filename( 1UV
561 subsystem='old settings.json'
562 ).name
563 new_name = cli_helpers.config_filename(subsystem='vault').name 1UV
564 self.deprecation_warning( 1UV
565 _msg.TranslatedString(
566 _msg.WarnMsgTemplate.V01_STYLE_CONFIG,
567 old=old_name,
568 new=new_name,
569 ),
570 )
571 if isinstance(exc, OSError): 1UV
572 self.warning( 1V
573 _msg.TranslatedString(
574 _msg.WarnMsgTemplate.FAILED_TO_MIGRATE_CONFIG,
575 path=new_name,
576 error=exc.strerror,
577 filename=exc.filename,
578 ).maybe_without_filename(),
579 )
580 else:
581 self.deprecation_info( 1U
582 _msg.TranslatedString(
583 _msg.InfoMsgTemplate.SUCCESSFULLY_MIGRATED,
584 path=new_name,
585 ),
586 )
587 return backup_config 1UV
588 except OSError as exc: 136
589 self.err( 16
590 _msg.TranslatedString(
591 _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS,
592 error=exc.strerror,
593 filename=exc.filename,
594 ).maybe_without_filename(),
595 )
596 except Exception as exc: # noqa: BLE001 13
597 self.err( 13
598 _msg.TranslatedString(
599 _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS,
600 error=str(exc),
601 filename=None,
602 ).maybe_without_filename(),
603 exc_info=exc,
604 )
606 def put_config(self, config: _types.VaultConfig, /) -> None:
607 """Store the given vault configuration to disk."""
608 try: 1adqpbfztxusmlc5ghEeyvikj
609 cli_helpers.save_config(config) 1adqpbfztxusmlc5ghEeyvikj
610 except OSError as exc: 1xuml
611 self.err( 1xm
612 _msg.TranslatedString(
613 _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS,
614 error=exc.strerror,
615 filename=exc.filename,
616 ).maybe_without_filename(),
617 )
618 except Exception as exc: # noqa: BLE001 1ul
619 self.err( 1ul
620 _msg.TranslatedString(
621 _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS,
622 error=str(exc),
623 filename=None,
624 ).maybe_without_filename(),
625 exc_info=exc,
626 )
628 def get_user_config(self) -> dict[str, Any]:
629 """Return the global user configuration stored on disk.
631 If no configuration is stored, return an empty configuration.
633 """
634 try: 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsmlcTr24JwOghEeyGvFikjoC
635 return cli_helpers.load_user_config() 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsmlcTr24JwOghEeyGvFikjoC
636 except FileNotFoundError: 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsml24JwOghEeyGvFikjoC
637 return {} 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsmlJwOghEeyGvFikjoC
638 except OSError as exc: 124
639 self.err( 14
640 _msg.TranslatedString(
641 _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG,
642 error=exc.strerror,
643 filename=exc.filename,
644 ).maybe_without_filename(),
645 )
646 except Exception as exc: # noqa: BLE001 12
647 self.err( 12
648 _msg.TranslatedString(
649 _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG,
650 error=str(exc),
651 filename=None,
652 ).maybe_without_filename(),
653 exc_info=exc,
654 )
656 def validate_command_line(self) -> None: # noqa: C901,PLR0912
657 """Check for missing/extra arguments and conflicting options.
659 Raises:
660 click.UsageError:
661 The command-line is invalid because of missing or extra
662 arguments, or because of conflicting options. The error
663 message contains further details.
665 """
666 param: click.Parameter
667 self.check_incompatible_options('--phrase', '--key') 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
668 for group in ( 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
669 cli_machinery.ConfigurationOption,
670 cli_machinery.StorageManagementOption,
671 ):
672 for opt in self.options_in_group[group]: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
673 if opt not in { 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
674 self.params_by_str['--config'],
675 self.params_by_str['--notes'],
676 }:
677 for other_opt in self.options_in_group[ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
678 cli_machinery.PassphraseGenerationOption
679 ]:
680 self.check_incompatible_options(opt, other_opt) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
682 for group in ( 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
683 cli_machinery.ConfigurationOption,
684 cli_machinery.StorageManagementOption,
685 ):
686 for opt in self.options_in_group[group]: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
687 for other_opt in self.options_in_group[ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
688 cli_machinery.ConfigurationOption
689 ]:
690 if {opt, other_opt} != { 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
691 self.params_by_str['--config'],
692 self.params_by_str['--notes'],
693 }:
694 self.check_incompatible_options(opt, other_opt) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
695 for other_opt in self.options_in_group[ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
696 cli_machinery.StorageManagementOption
697 ]:
698 self.check_incompatible_options(opt, other_opt) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
699 service: str | None = self.ctx.params['service'] 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
700 service_metavar = _msg.TranslatedString( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
701 _msg.Label.VAULT_METAVAR_SERVICE
702 )
703 sv_or_global_options = self.options_in_group[ 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
704 cli_machinery.PassphraseGenerationOption
705 ]
706 for param in sv_or_global_options: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
707 if self.is_param_set(param) and not ( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
708 service is not None
709 or self.is_param_set(self.params_by_str['--config'])
710 ):
711 err_msg = _msg.TranslatedString( 1b
712 _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE_OR_CONFIG,
713 param=param.opts[0],
714 service_metavar=service_metavar,
715 )
716 raise click.UsageError(str(err_msg)) 1b
717 sv_options = [ 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
718 self.params_by_str['--notes'],
719 self.params_by_str['--delete'],
720 ]
721 for param in sv_options: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
722 if self.is_param_set(param) and not service is not None: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
723 err_msg = _msg.TranslatedString( 1b
724 _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE,
725 param=param.opts[0],
726 service_metavar=service_metavar,
727 )
728 raise click.UsageError(str(err_msg)) 1b
729 no_sv_options = [ 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
730 self.params_by_str['--delete-globals'],
731 self.params_by_str['--clear'],
732 *self.options_in_group[cli_machinery.StorageManagementOption],
733 ]
734 for param in no_sv_options: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
735 if self.is_param_set(param) and service is not None: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
736 err_msg = _msg.TranslatedString( 1b
737 _msg.ErrMsgTemplate.PARAMS_NO_SERVICE,
738 param=param.opts[0],
739 service_metavar=service_metavar,
740 )
741 raise click.UsageError(str(err_msg)) 1b
743 def get_mutex(
744 self,
745 op: str,
746 ) -> AbstractContextManager[AbstractContextManager | None]:
747 """Return a mutex for accessing the configuration on disk.
749 The mutex is a context manager, and will lock out other threads
750 and processes attempting to access the configuration in an
751 incompatible manner.
753 Returns:
754 If the requested operation is a read-only operation, return
755 a no-op mutex. (Concurrent reads are always allowed, even
756 in the presence of writers.) Otherwise, for read-write
757 operations, return an actual mutex.
759 """
760 return ( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
761 cli_helpers.configuration_mutex()
762 if op in self.readwrite_ops
763 else contextlib.nullcontext()
764 )
766 def dispatch_op(self) -> None:
767 """Dispatch to the handler matching the command-line call.
769 Also issue any appropriate warnings about the command-line,
770 e.g., incompatibilities with vault(1) or ineffective options.
772 """
773 service_metavar = _msg.TranslatedString( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
774 _msg.Label.VAULT_METAVAR_SERVICE
775 )
776 if self.ctx.params['service'] == '': # noqa: PLC1901 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
777 self.warning( 1f
778 _msg.TranslatedString(
779 _msg.WarnMsgTemplate.EMPTY_SERVICE_NOT_SUPPORTED,
780 service_metavar=service_metavar,
781 ),
782 )
783 if self.ctx.params.get('edit_notes') and not self.ctx.params.get( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
784 'store_config_only'
785 ):
786 self.warning( 1C
787 _msg.TranslatedString(
788 _msg.WarnMsgTemplate.EDITING_NOTES_BUT_NOT_STORING_CONFIG,
789 service_metavar=service_metavar,
790 ),
791 )
792 op: str
793 for candidate_op in self.all_ops[:-1]: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
794 if self.ctx.params.get(candidate_op): 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
795 op = candidate_op 1adqpbfzYZ17W360ntDPxuKQRSsmlcTrJUV5ghEeyvikjo
796 break 1adqpbfzYZ17W360ntDPxuKQRSsmlcTrJUV5ghEeyvikjo
797 else:
798 op = self.all_ops[-1] 1ABHLMIbXNcr24wOGFC
799 with self.get_mutex(op): 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
800 op_func = getattr(self, 'run_op_' + op) 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
801 return op_func() 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
803 def run_op_delete_service_settings(self) -> None:
804 """Delete settings for a specific service."""
805 service = self.ctx.params['service'] 1ad5
806 assert service is not None 1ad5
807 configuration = self.get_config() 1ad5
808 if service in configuration['services']: 1ad5
809 del configuration['services'][service] 1ad5
810 self.put_config(configuration) 1ad5
812 def run_op_delete_globals(self) -> None:
813 """Delete the global settings."""
814 configuration = self.get_config() 1adb5
815 if 'global' in configuration: 1adb5
816 del configuration['global'] 1adb5
817 self.put_config(configuration) 1adb5
819 def run_op_clear_all_settings(self) -> None:
820 """Clear all settings."""
821 self.put_config({'services': {}}) 1adb5hEey
823 def run_op_import_settings(self) -> None: # noqa: C901,PLR0912
824 """Import settings from a given file.
826 Issue multiple warnings, if appropriate, e.g. for Unicode
827 normalization issues with stored passphrases or conflicting
828 stored passphrases and keys. Respect the `--overwrite-config`
829 and `--merge-config` options when writing the imported
830 configuration to disk.
832 """
833 service_metavar = _msg.TranslatedString( 1adqbfzYZ1cTghEeyv
834 _msg.Label.VAULT_METAVAR_SERVICE
835 )
836 user_config = self.get_user_config() 1adqbfzYZ1cTghEeyv
837 import_settings = self.ctx.params['import_settings'] 1adqbfzYZ1cTghEeyv
838 overwrite_config = self.ctx.params['overwrite_config'] 1adqbfzYZ1cTghEeyv
839 try: 1adqbfzYZ1cTghEeyv
840 # TODO(the-13th-letter): keep track of auto-close; try
841 # os.dup if feasible
842 infile = cast( 1adqbfzYZ1cTghEeyv
843 'TextIO',
844 (
845 import_settings
846 if hasattr(import_settings, 'close')
847 else click.open_file(os.fspath(import_settings), 'rt')
848 ),
849 )
850 # Don't specifically catch TypeError or ValueError here if
851 # the passed-in fileobj is not a readable text stream. This
852 # will never happen on the command-line (thanks to `click`),
853 # and for programmatic use, our caller may want accurate
854 # error information.
855 with infile: 1adqbfzYZcTghEeyv
856 maybe_config = json.load(infile) 1adqbfzYZcTghEeyv
857 except json.JSONDecodeError as exc: 1Z1
858 self.err( 1Z
859 _msg.TranslatedString(
860 _msg.ErrMsgTemplate.CANNOT_DECODEIMPORT_VAULT_SETTINGS,
861 error=exc,
862 )
863 )
864 except OSError as exc: 11
865 self.err( 11
866 _msg.TranslatedString(
867 _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS,
868 error=exc.strerror,
869 filename=exc.filename,
870 ).maybe_without_filename()
871 )
872 cleaned = _types.clean_up_falsy_vault_config_values(maybe_config) 1adqbfzYcTghEeyv
873 if not _types.is_vault_config(maybe_config): 1adqbfzYcTghEeyv
874 self.err( 1Y
875 _msg.TranslatedString(
876 _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS,
877 error=_msg.TranslatedString(
878 _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG,
879 config=maybe_config,
880 ),
881 filename=None,
882 ).maybe_without_filename()
883 )
884 assert cleaned is not None 1adqbfzcTghEeyv
885 for step in cleaned: 1adqbfzcTghEeyv
886 # These are never fatal errors, because the semantics of
887 # vault upon encountering these settings are ill-specified,
888 # but not ill-defined.
889 if step.action == 'replace': 1eyv
890 self.warning( 1eyv
891 _msg.TranslatedString(
892 _msg.WarnMsgTemplate.STEP_REPLACE_INVALID_VALUE,
893 old=json.dumps(step.old_value),
894 path=_types.json_path(step.path),
895 new=json.dumps(step.new_value),
896 ),
897 )
898 else:
899 self.warning( 1v
900 _msg.TranslatedString(
901 _msg.WarnMsgTemplate.STEP_REMOVE_INEFFECTIVE_VALUE,
902 path=_types.json_path(step.path),
903 old=json.dumps(step.old_value),
904 ),
905 )
906 if '' in maybe_config['services']: 1adqbfzcTghEeyv
907 self.warning( 1qf
908 _msg.TranslatedString(
909 _msg.WarnMsgTemplate.EMPTY_SERVICE_SETTINGS_INACCESSIBLE,
910 service_metavar=service_metavar,
911 PROG_NAME=PROG_NAME,
912 ),
913 )
914 for service_name in sorted(maybe_config['services'].keys()): 1adqbfzcTghEeyv
915 if not cli_helpers.is_completable_item(service_name): 1adqfzcTgeyv
916 self.warning( 1g
917 _msg.TranslatedString(
918 _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,
919 service=service_name,
920 ),
921 )
922 try: 1adqbfzcTghEeyv
923 cli_helpers.check_for_misleading_passphrase( 1adqbfzcTghEeyv
924 ('global',),
925 cast('dict[str, Any]', maybe_config.get('global', {})),
926 main_config=user_config,
927 ctx=self.ctx,
928 )
929 for key, value in maybe_config['services'].items(): 1adqbfzcTghEeyv
930 cli_helpers.check_for_misleading_passphrase( 1adqfzcTgeyv
931 ('services', key),
932 cast('dict[str, Any]', value),
933 main_config=user_config,
934 ctx=self.ctx,
935 )
936 except AssertionError as exc: 1T
937 self.err( 1T
938 _msg.TranslatedString(
939 _msg.ErrMsgTemplate.INVALID_USER_CONFIG,
940 error=exc,
941 filename=None,
942 ).maybe_without_filename(),
943 )
944 global_obj = maybe_config.get('global', {}) 1adqbfzcghEeyv
945 has_key = _types.js_truthiness(global_obj.get('key')) 1adqbfzcghEeyv
946 has_phrase = _types.js_truthiness(global_obj.get('phrase')) 1adqbfzcghEeyv
947 if has_key and has_phrase: 1adqbfzcghEeyv
948 self.warning( 1zhEv
949 _msg.TranslatedString(
950 _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE,
951 ),
952 )
953 for service_name, service_obj in maybe_config['services'].items(): 1adqbfzcghEeyv
954 has_key = _types.js_truthiness( 1adqfzcgeyv
955 service_obj.get('key')
956 ) or _types.js_truthiness(global_obj.get('key'))
957 has_phrase = _types.js_truthiness( 1adqfzcgeyv
958 service_obj.get('phrase')
959 ) or _types.js_truthiness(global_obj.get('phrase'))
960 if has_key and has_phrase: 1adqfzcgeyv
961 self.warning( 1zyv
962 _msg.TranslatedString(
963 _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE,
964 service=json.dumps(service_name),
965 ),
966 )
967 if overwrite_config: 1adqbfzcghEeyv
968 self.put_config(maybe_config) 1ad
969 else:
970 configuration = self.get_config() 1adqbfzcghEeyv
971 merged_config: collections.ChainMap[str, Any] = ( 1adqbfzcghEeyv
972 collections.ChainMap(
973 {
974 'services': collections.ChainMap(
975 maybe_config['services'],
976 configuration['services'],
977 ),
978 },
979 {'global': maybe_config['global']}
980 if 'global' in maybe_config
981 else {},
982 {'global': configuration['global']}
983 if 'global' in configuration
984 else {},
985 )
986 )
987 new_config: Any = { 1adqbfzcghEeyv
988 k: dict(v) if isinstance(v, collections.ChainMap) else v
989 for k, v in sorted(merged_config.items())
990 }
991 assert _types.is_vault_config(new_config) 1adqbfzcghEeyv
992 self.put_config(new_config) 1adqbfzcghEeyv
994 def run_op_export_settings(self) -> None:
995 """Export settings to a given file.
997 Respect the `--export-as` option when writing the exported
998 configuration to the file.
1000 """
1001 export_settings = self.ctx.params['export_settings'] 1b7W360UV
1002 export_as = self.ctx.params['export_as'] 1b7W360UV
1003 configuration = self.get_config() 1b7W360UV
1004 try: 1b7W0UV
1005 # TODO(the-13th-letter): keep track of auto-close; try
1006 # os.dup if feasible
1007 outfile = cast( 1b7W0UV
1008 'TextIO',
1009 (
1010 export_settings
1011 if hasattr(export_settings, 'close')
1012 else click.open_file(os.fspath(export_settings), 'wt')
1013 ),
1014 )
1015 # Don't specifically catch TypeError or ValueError here if
1016 # the passed-in fileobj is not a writable text stream. This
1017 # will never happen on the command-line (thanks to `click`),
1018 # and for programmatic use, our caller may want accurate
1019 # error information.
1020 with outfile: 1b7WUV
1021 if export_as == 'sh': 1b7WUV
1022 this_ctx = self.ctx 1W
1023 prog_name_pieces = collections.deque([ 1W
1024 this_ctx.info_name or 'vault',
1025 ])
1026 while ( 1W
1027 this_ctx.parent is not None
1028 and this_ctx.parent.info_name is not None
1029 ):
1030 prog_name_pieces.appendleft(this_ctx.parent.info_name) 1W
1031 this_ctx = this_ctx.parent 1W
1032 cli_helpers.print_config_as_sh_script( 1W
1033 configuration,
1034 outfile=outfile,
1035 prog_name_list=prog_name_pieces,
1036 )
1037 else:
1038 json.dump( 1b7WUV
1039 configuration,
1040 outfile,
1041 ensure_ascii=False,
1042 indent=2,
1043 sort_keys=True,
1044 )
1045 except OSError as exc: 10
1046 self.err( 10
1047 _msg.TranslatedString(
1048 _msg.ErrMsgTemplate.CANNOT_EXPORT_VAULT_SETTINGS,
1049 error=exc.strerror,
1050 filename=exc.filename,
1051 ).maybe_without_filename(),
1052 )
1054 def run_subop_query_phrase_or_key_change(
1055 self,
1056 *,
1057 empty_service_permitted: bool,
1058 configuration: _types.VaultConfig | None = None,
1059 ) -> collections.ChainMap[str, Any]:
1060 """Query the master passphrase or master SSH key, if changed.
1062 If the user indicates they want to change the master passphrase
1063 or master SSH key for this call, or if they want to configure
1064 a stored global or service-specific master passphrase or master
1065 SSH key, then query the user.
1067 This is not a complete command-line call operation in and of
1068 itself.
1070 Args:
1071 empty_service_permitted:
1072 True if an empty service name is permitted, False otherwise.
1073 configuration:
1074 The vault configuration, parsed from disk. If not
1075 given, we read the configuration from disk ourselves.
1077 The returned effective configuration already contains
1078 a relevant slice of the vault configuration. However,
1079 some callers need access to the full configuration *and*
1080 need the slice within the effective configuration to
1081 refer to the same object.
1083 Returns:
1084 The effective configuration for the (possibly empty) given
1085 service, as a [chained map][collections.ChainMap]. Any
1086 master passphrase or master SSH key overrides that may be in
1087 effect are stored in the first map.
1089 Raises:
1090 click.UsageError:
1091 The service name was empty, and an empty service name
1092 was not permitted as per the method parameters.
1094 Warning:
1095 It is the caller's responsibility to vet any interactively
1096 entered master passphrase for Unicode normalization issues.
1098 """
1099 service = self.ctx.params['service'] 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC
1100 use_key = self.ctx.params['use_key'] 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC
1101 use_phrase = self.ctx.params['use_phrase'] 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC
1102 if configuration is None: 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC
1103 configuration = self.get_config() 1ABHLMIbXNcrwOGFC
1104 service_keys_on_commandline = { 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC
1105 'length',
1106 'repeat',
1107 'lower',
1108 'upper',
1109 'number',
1110 'space',
1111 'dash',
1112 'symbol',
1113 }
1114 settings: collections.ChainMap[str, Any] = collections.ChainMap( 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC
1115 {
1116 k: v
1117 for k in service_keys_on_commandline
1118 if (v := self.ctx.params.get(k)) is not None
1119 },
1120 cast(
1121 'dict[str, Any]',
1122 configuration['services'].get(service, {}) if service else {},
1123 ),
1124 cast('dict[str, Any]', configuration.get('global', {})),
1125 )
1126 if not service and not empty_service_permitted: 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC
1127 err_msg = _msg.TranslatedString( 1XO
1128 _msg.ErrMsgTemplate.SERVICE_REQUIRED,
1129 service_metavar=_msg.TranslatedString(
1130 _msg.Label.VAULT_METAVAR_SERVICE
1131 ),
1132 )
1133 raise click.UsageError(str(err_msg)) 1XO
1134 if use_key: 1adABHLMIpbfntDPxuKQRSNsmlcrJwgheGFikjoC
1135 settings.maps[0]['key'] = base64.standard_b64encode( 1LMtDPQRSJ
1136 cli_helpers.select_ssh_key(
1137 ctx=self.ctx,
1138 error_callback=self.err,
1139 warning_callback=self.warning,
1140 )
1141 ).decode('ASCII')
1142 elif use_phrase: 1adABHIpbfntDxuKNsmlcrwgheGFikjoC
1143 maybe_phrase = cli_helpers.prompt_for_passphrase() 1ABpbtDsmlcrw
1144 if not maybe_phrase: 1ABpbtDsmlcrw
1145 self.err( 1D
1146 _msg.TranslatedString(
1147 _msg.ErrMsgTemplate.USER_ABORTED_PASSPHRASE
1148 )
1149 )
1150 else:
1151 settings.maps[0]['phrase'] = maybe_phrase 1ABpbtsmlcrw
1152 return settings 1adABHLMIpbfntDxuKNsmlcrwgheGFikjoC
1154 def run_op_store_config_only(self) -> None: # noqa: C901,PLR0912,PLR0914,PLR0915
1155 """Update the stored vault configuration.
1157 Depending on the presence or the absence of a service name,
1158 update either the service-specific settings, or the global
1159 settings. (An empty service name is respected, i.e., updates
1160 the former.)
1162 Respect the `--unset=SETTING` option to unset the named
1163 settings, and the `--notes` option to edit notes interactively
1164 in a spawned text editor. Issue multiple warnings, if
1165 appropriate, e.g. for Unicode normalization issues with stored
1166 passphrases or conflicting stored passphrases and keys. Respect
1167 the `--overwrite-config` and `--merge-config` options when
1168 writing the imported configuration to disk, and the
1169 `--modern-editor-interface` and
1170 `--vault-legacy-editor-interface` options when editing notes.
1172 Raises:
1173 click.UsageError:
1174 The user requested that the same setting be both unset
1175 and set.
1177 """
1178 service = self.ctx.params['service'] 1adpbfntDPxuKQRSsmlcrJgheikjo
1179 use_key = self.ctx.params['use_key'] 1adpbfntDPxuKQRSsmlcrJgheikjo
1180 use_phrase = self.ctx.params['use_phrase'] 1adpbfntDPxuKQRSsmlcrJgheikjo
1181 unset_settings = self.ctx.params['unset_settings'] 1adpbfntDPxuKQRSsmlcrJgheikjo
1182 overwrite_config = self.ctx.params['overwrite_config'] 1adpbfntDPxuKQRSsmlcrJgheikjo
1183 edit_notes = self.ctx.params['edit_notes'] 1adpbfntDPxuKQRSsmlcrJgheikjo
1184 modern_editor_interface = self.ctx.params['modern_editor_interface'] 1adpbfntDPxuKQRSsmlcrJgheikjo
1185 configuration = self.get_config() 1adpbfntDPxuKQRSsmlcrJgheikjo
1186 user_config = self.get_user_config() 1adpbfntDPxuKQRSsmlcrJgheikjo
1187 settings = self.run_subop_query_phrase_or_key_change( 1adpbfntDPxuKQRSsmlcrJgheikjo
1188 configuration=configuration, empty_service_permitted=True
1189 )
1190 overrides = settings.maps[0] 1adpbfntDxuKsmlcrgheikjo
1191 view: collections.ChainMap[str, Any]
1192 view = ( 1adpbfntDxuKsmlcrgheikjo
1193 collections.ChainMap(*settings.maps[:2])
1194 if service
1195 else collections.ChainMap(settings.maps[0], settings.maps[2])
1196 )
1197 if use_key: 1adpbfntDxuKsmlcrgheikjo
1198 view['key'] = overrides['key'] 1t
1199 elif use_phrase: 1adpbfntDxuKsmlcrgheikjo
1200 view['phrase'] = overrides['phrase'] 1pbtsmlcr
1201 try: 1pbtsmlcr
1202 cli_helpers.check_for_misleading_passphrase( 1pbtsmlcr
1203 ('services', service) if service else ('global',),
1204 overrides,
1205 main_config=user_config,
1206 ctx=self.ctx,
1207 )
1208 except AssertionError as exc: 1r
1209 self.err( 1r
1210 _msg.TranslatedString(
1211 _msg.ErrMsgTemplate.INVALID_USER_CONFIG,
1212 error=exc,
1213 filename=None,
1214 ).maybe_without_filename(),
1215 )
1216 if 'key' in settings: 1pbtsmlc
1217 if service: 1p
1218 w_msg = _msg.TranslatedString( 1p
1219 _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE,
1220 service=json.dumps(service),
1221 )
1222 else:
1223 w_msg = _msg.TranslatedString( 1p
1224 _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE
1225 )
1226 self.warning(w_msg) 1p
1227 if not view.maps[0] and not unset_settings and not edit_notes: 1adpbfntDxuKsmlcgheikjo
1228 err_msg = _msg.TranslatedString( 1D
1229 _msg.ErrMsgTemplate.CANNOT_UPDATE_SETTINGS_NO_SETTINGS,
1230 settings_type=_msg.TranslatedString(
1231 _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE
1232 if service
1233 else _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL # noqa: E501
1234 ),
1235 )
1236 raise click.UsageError(str(err_msg)) 1D
1237 for setting in unset_settings: 1adpbfntxuKsmlcgheikjo
1238 if setting in view.maps[0]: 1adK
1239 err_msg = _msg.TranslatedString( 1K
1240 _msg.ErrMsgTemplate.SET_AND_UNSET_SAME_SETTING,
1241 setting=setting,
1242 )
1243 raise click.UsageError(str(err_msg)) 1K
1244 if not cli_helpers.is_completable_item(service): 1adpbfntxusmlcgheikjo
1245 self.warning( 1g
1246 _msg.TranslatedString(
1247 _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,
1248 service=service,
1249 ),
1250 )
1251 subtree: dict[str, Any] = ( 1adpbfntxusmlcgheikjo
1252 configuration['services'].setdefault(service, {}) # type: ignore[assignment]
1253 if service
1254 else configuration.setdefault('global', {})
1255 )
1256 if overwrite_config: 1adpbfntxusmlcgheikjo
1257 subtree.clear() 1ad
1258 else:
1259 for setting in unset_settings: 1adpbfntxusmlcgheikjo
1260 subtree.pop(setting, None) 1ad
1261 subtree.update(view) 1adpbfntxusmlcgheikjo
1262 assert _types.is_vault_config(configuration), ( 1adpbfntxusmlcgheikjo
1263 f'Invalid vault configuration: {configuration!r}'
1264 )
1265 if edit_notes: 1adpbfntxusmlcgheikjo
1266 assert service is not None 1nikjo
1267 notes_instructions = _msg.TranslatedString( 1nikjo
1268 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT
1269 )
1270 notes_marker = _msg.TranslatedString( 1nikjo
1271 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER
1272 )
1273 notes_legacy_instructions = _msg.TranslatedString( 1nikjo
1274 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT
1275 )
1276 old_notes_value = subtree.get('notes', '') 1nikjo
1277 if modern_editor_interface: 1nikjo
1278 text = '\n'.join([ 1nikjo
1279 str(notes_instructions),
1280 str(notes_marker),
1281 old_notes_value,
1282 ])
1283 else:
1284 text = old_notes_value or str(notes_legacy_instructions) 1ikj
1285 notes_value = click.edit(text=text, require_save=False) 1nikjo
1286 assert notes_value is not None 1nikjo
1287 if ( 1nikjo
1288 not modern_editor_interface
1289 and notes_value.strip() != old_notes_value.strip()
1290 ):
1291 backup_file = cli_helpers.config_filename( 1ij
1292 subsystem='notes backup'
1293 )
1294 backup_file.write_text(old_notes_value, encoding='UTF-8') 1ij
1295 self.warning( 1ij
1296 _msg.TranslatedString(
1297 _msg.WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP,
1298 filename=str(backup_file),
1299 ),
1300 )
1301 subtree['notes'] = notes_value.strip() 1ij
1302 elif ( 1nikjo
1303 modern_editor_interface and notes_value.strip() != text.strip()
1304 ):
1305 notes_lines = collections.deque( 1nikjo
1306 notes_value.splitlines(keepends=True)
1307 )
1308 while notes_lines: 1nikjo
1309 line = notes_lines.popleft() 1ikj
1310 if line.startswith(str(notes_marker)): 1ikj
1311 notes_value = ''.join(notes_lines) 1i
1312 break 1i
1313 else:
1314 if not notes_value.strip(): 1nkjo
1315 self.err( 1nko
1316 _msg.TranslatedString(
1317 _msg.ErrMsgTemplate.USER_ABORTED_EDIT
1318 )
1319 )
1320 subtree['notes'] = notes_value.strip() 1ikj
1321 self.put_config(configuration) 1adpbftxusmlcgheikj
1323 def run_op_derive_passphrase(self) -> None:
1324 """Derive a passphrase.
1326 Derive a service passphrase using the effective settings from
1327 both the command-line and the stored configuration. If any
1328 service notes are stored for this service, print them as well.
1330 (An empty service name is permitted, though discouraged for
1331 compatibility reasons.)
1333 Issue a warning (if appropriate) for Unicode normalization
1334 issues with the interactive passphrase. Respect the
1335 `--print-notes-before` and `--print-notes-after` options when
1336 printing notes.
1338 Raises:
1339 click.UsageError:
1340 No master passphrase or master SSH key was given on both
1341 the command-line and in the vault configuration on disk.
1342 """
1343 service = self.ctx.params['service'] 1ABHLMIbXNcr24wOGFC
1344 use_key = self.ctx.params['use_key'] 1ABHLMIbXNcr24wOGFC
1345 use_phrase = self.ctx.params['use_phrase'] 1ABHLMIbXNcr24wOGFC
1346 print_notes_before = self.ctx.params['print_notes_before'] 1ABHLMIbXNcr24wOGFC
1347 user_config = self.get_user_config() 1ABHLMIbXNcr24wOGFC
1348 settings = self.run_subop_query_phrase_or_key_change( 1ABHLMIbXNcrwOGFC
1349 empty_service_permitted=False
1350 )
1351 if use_phrase: 1ABHLMIbNcrwGFC
1352 try: 1ABbcrw
1353 cli_helpers.check_for_misleading_passphrase( 1ABbcrw
1354 cli_helpers.ORIGIN.INTERACTIVE,
1355 {'phrase': settings['phrase']},
1356 main_config=user_config,
1357 ctx=self.ctx,
1358 )
1359 except AssertionError as exc: 1r
1360 self.err( 1r
1361 _msg.TranslatedString(
1362 _msg.ErrMsgTemplate.INVALID_USER_CONFIG,
1363 error=exc,
1364 filename=None,
1365 ).maybe_without_filename(),
1366 )
1367 phrase: str | bytes
1368 overrides = cast('dict[str, int | str]', settings.maps[0]) 1ABHLMIbNcwGFC
1369 # If either --key or --phrase are given, use that setting.
1370 # Otherwise, if both key and phrase are set in the config,
1371 # use the key. Otherwise, if only one of key and phrase is
1372 # set in the config, use that one. In all these above
1373 # cases, set the phrase via vault.Vault.phrase_from_key if
1374 # a key is given. Finally, if nothing is set, error out.
1375 if use_key: 1ABHLMIbNcwGFC
1376 phrase = cli_helpers.key_to_phrase( 1LM
1377 cast('str', overrides['key']),
1378 error_callback=self.err,
1379 warning_callback=self.warning,
1380 )
1381 elif use_phrase: 1ABHIbNcwGFC
1382 phrase = cast('str', overrides['phrase']) 1ABbcw
1383 elif settings.get('key'): 1HIbNGFC
1384 phrase = cli_helpers.key_to_phrase( 1HI
1385 cast('str', settings['key']), error_callback=self.err
1386 )
1387 elif settings.get('phrase'): 1bNGFC
1388 phrase = cast('str', settings['phrase']) 1bGFC
1389 else:
1390 err_msg = _msg.TranslatedString( 1N
1391 _msg.ErrMsgTemplate.NO_KEY_OR_PHRASE
1392 )
1393 raise click.UsageError(str(err_msg)) 1N
1394 overrides.pop('key', '') 1ABHLMIbcwGFC
1395 overrides.pop('phrase', '') 1ABHLMIbcwGFC
1396 assert service is not None 1ABHLMIbcwGFC
1397 vault_service_keys = { 1ABHLMIbcwGFC
1398 'length',
1399 'repeat',
1400 'lower',
1401 'upper',
1402 'number',
1403 'space',
1404 'dash',
1405 'symbol',
1406 }
1407 kwargs = { 1ABHLMIbcwGFC
1408 cast('str', k): cast('int', settings[k])
1409 for k in vault_service_keys
1410 if k in settings
1411 }
1412 result = vault.Vault(phrase=phrase, **kwargs).generate(service) 1ABHLMIbcwGFC
1413 service_notes = cast('str', settings.get('notes', '')).strip() 1ABHLMIbcwGFC
1414 if print_notes_before and service_notes.strip(): 1ABHLMIbcwGFC
1415 click.echo(f'{service_notes}\n', err=True, color=self.ctx.color) 1F
1416 click.echo(result.decode('ASCII'), color=self.ctx.color) 1ABHLMIbcwGFC
1417 if not print_notes_before and service_notes.strip(): 1ABHLMIbcwGFC
1418 click.echo(f'\n{service_notes}\n', err=True, color=self.ctx.color) 1GFC
1421@derivepassphrase.command(
1422 'vault',
1423 context_settings={'help_option_names': ['-h', '--help']},
1424 cls=cli_machinery.CommandWithHelpGroups,
1425 help=(
1426 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_01),
1427 _msg.TranslatedString(
1428 _msg.Label.DERIVEPASSPHRASE_VAULT_02,
1429 service_metavar=_msg.TranslatedString(
1430 _msg.Label.VAULT_METAVAR_SERVICE
1431 ),
1432 ),
1433 ),
1434 epilog=(
1435 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_01),
1436 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_02),
1437 ),
1438)
1439@click.option(
1440 '-p',
1441 '--phrase',
1442 'use_phrase',
1443 is_flag=True,
1444 help=_msg.TranslatedString(
1445 _msg.Label.DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT
1446 ),
1447 cls=cli_machinery.PassphraseGenerationOption,
1448)
1449@click.option(
1450 '-k',
1451 '--key',
1452 'use_key',
1453 is_flag=True,
1454 help=_msg.TranslatedString(
1455 _msg.Label.DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT
1456 ),
1457 cls=cli_machinery.PassphraseGenerationOption,
1458)
1459@click.option(
1460 '-l',
1461 '--length',
1462 metavar=_msg.TranslatedString(
1463 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1464 ),
1465 callback=cli_machinery.validate_length,
1466 help=_msg.TranslatedString(
1467 _msg.Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT,
1468 metavar=_msg.TranslatedString(
1469 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1470 ),
1471 ),
1472 cls=cli_machinery.PassphraseGenerationOption,
1473)
1474@click.option(
1475 '-r',
1476 '--repeat',
1477 metavar=_msg.TranslatedString(
1478 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1479 ),
1480 callback=cli_machinery.validate_occurrence_constraint,
1481 help=_msg.TranslatedString(
1482 _msg.Label.DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT,
1483 metavar=_msg.TranslatedString(
1484 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1485 ),
1486 ),
1487 cls=cli_machinery.PassphraseGenerationOption,
1488)
1489@click.option(
1490 '--lower',
1491 metavar=_msg.TranslatedString(
1492 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1493 ),
1494 callback=cli_machinery.validate_occurrence_constraint,
1495 help=_msg.TranslatedString(
1496 _msg.Label.DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT,
1497 metavar=_msg.TranslatedString(
1498 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1499 ),
1500 ),
1501 cls=cli_machinery.PassphraseGenerationOption,
1502)
1503@click.option(
1504 '--upper',
1505 metavar=_msg.TranslatedString(
1506 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1507 ),
1508 callback=cli_machinery.validate_occurrence_constraint,
1509 help=_msg.TranslatedString(
1510 _msg.Label.DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT,
1511 metavar=_msg.TranslatedString(
1512 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1513 ),
1514 ),
1515 cls=cli_machinery.PassphraseGenerationOption,
1516)
1517@click.option(
1518 '--number',
1519 metavar=_msg.TranslatedString(
1520 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1521 ),
1522 callback=cli_machinery.validate_occurrence_constraint,
1523 help=_msg.TranslatedString(
1524 _msg.Label.DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT,
1525 metavar=_msg.TranslatedString(
1526 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1527 ),
1528 ),
1529 cls=cli_machinery.PassphraseGenerationOption,
1530)
1531@click.option(
1532 '--space',
1533 metavar=_msg.TranslatedString(
1534 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1535 ),
1536 callback=cli_machinery.validate_occurrence_constraint,
1537 help=_msg.TranslatedString(
1538 _msg.Label.DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT,
1539 metavar=_msg.TranslatedString(
1540 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1541 ),
1542 ),
1543 cls=cli_machinery.PassphraseGenerationOption,
1544)
1545@click.option(
1546 '--dash',
1547 metavar=_msg.TranslatedString(
1548 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1549 ),
1550 callback=cli_machinery.validate_occurrence_constraint,
1551 help=_msg.TranslatedString(
1552 _msg.Label.DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT,
1553 metavar=_msg.TranslatedString(
1554 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1555 ),
1556 ),
1557 cls=cli_machinery.PassphraseGenerationOption,
1558)
1559@click.option(
1560 '--symbol',
1561 metavar=_msg.TranslatedString(
1562 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1563 ),
1564 callback=cli_machinery.validate_occurrence_constraint,
1565 help=_msg.TranslatedString(
1566 _msg.Label.DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT,
1567 metavar=_msg.TranslatedString(
1568 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1569 ),
1570 ),
1571 cls=cli_machinery.PassphraseGenerationOption,
1572)
1573@click.option(
1574 '-n',
1575 '--notes',
1576 'edit_notes',
1577 is_flag=True,
1578 help=_msg.TranslatedString(
1579 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT,
1580 service_metavar=_msg.TranslatedString(
1581 _msg.Label.VAULT_METAVAR_SERVICE
1582 ),
1583 ),
1584 cls=cli_machinery.ConfigurationOption,
1585)
1586@click.option(
1587 '-c',
1588 '--config',
1589 'store_config_only',
1590 is_flag=True,
1591 help=_msg.TranslatedString(
1592 _msg.Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT,
1593 service_metavar=_msg.TranslatedString(
1594 _msg.Label.VAULT_METAVAR_SERVICE
1595 ),
1596 ),
1597 cls=cli_machinery.ConfigurationOption,
1598)
1599@click.option(
1600 '-x',
1601 '--delete',
1602 'delete_service_settings',
1603 is_flag=True,
1604 help=_msg.TranslatedString(
1605 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT,
1606 service_metavar=_msg.TranslatedString(
1607 _msg.Label.VAULT_METAVAR_SERVICE
1608 ),
1609 ),
1610 cls=cli_machinery.ConfigurationOption,
1611)
1612@click.option(
1613 '--delete-globals',
1614 is_flag=True,
1615 help=_msg.TranslatedString(
1616 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT,
1617 ),
1618 cls=cli_machinery.ConfigurationOption,
1619)
1620@click.option(
1621 '-X',
1622 '--clear',
1623 'clear_all_settings',
1624 is_flag=True,
1625 help=_msg.TranslatedString(
1626 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT,
1627 ),
1628 cls=cli_machinery.ConfigurationOption,
1629)
1630@click.option(
1631 '-e',
1632 '--export',
1633 'export_settings',
1634 metavar=_msg.TranslatedString(
1635 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1636 ),
1637 help=_msg.TranslatedString(
1638 _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT,
1639 metavar=_msg.TranslatedString(
1640 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1641 ),
1642 ),
1643 cls=cli_machinery.StorageManagementOption,
1644 shell_complete=cli_helpers.shell_complete_path,
1645)
1646@click.option(
1647 '-i',
1648 '--import',
1649 'import_settings',
1650 metavar=_msg.TranslatedString(
1651 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1652 ),
1653 help=_msg.TranslatedString(
1654 _msg.Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT,
1655 metavar=_msg.TranslatedString(
1656 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1657 ),
1658 ),
1659 cls=cli_machinery.StorageManagementOption,
1660 shell_complete=cli_helpers.shell_complete_path,
1661)
1662@click.option(
1663 '--overwrite-existing/--merge-existing',
1664 'overwrite_config',
1665 default=False,
1666 help=_msg.TranslatedString(
1667 _msg.Label.DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT
1668 ),
1669 cls=cli_machinery.CompatibilityOption,
1670)
1671@click.option(
1672 '--unset',
1673 'unset_settings',
1674 multiple=True,
1675 type=click.Choice([
1676 'phrase',
1677 'key',
1678 'length',
1679 'repeat',
1680 'lower',
1681 'upper',
1682 'number',
1683 'space',
1684 'dash',
1685 'symbol',
1686 'notes',
1687 ]),
1688 help=_msg.TranslatedString(
1689 _msg.Label.DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT
1690 ),
1691 cls=cli_machinery.CompatibilityOption,
1692)
1693@click.option(
1694 '--export-as',
1695 type=click.Choice(['json', 'sh']),
1696 default='json',
1697 help=_msg.TranslatedString(
1698 _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT
1699 ),
1700 cls=cli_machinery.CompatibilityOption,
1701)
1702@click.option(
1703 '--modern-editor-interface/--vault-legacy-editor-interface',
1704 'modern_editor_interface',
1705 default=False,
1706 help=_msg.TranslatedString(
1707 _msg.Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT
1708 ),
1709 cls=cli_machinery.CompatibilityOption,
1710)
1711@click.option(
1712 '--print-notes-before/--print-notes-after',
1713 'print_notes_before',
1714 default=False,
1715 help=_msg.TranslatedString(
1716 _msg.Label.DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT,
1717 service_metavar=_msg.TranslatedString(
1718 _msg.Label.VAULT_METAVAR_SERVICE
1719 ),
1720 ),
1721 cls=cli_machinery.CompatibilityOption,
1722)
1723@cli_machinery.version_option(cli_machinery.vault_version_option_callback)
1724@cli_machinery.color_forcing_pseudo_option
1725@cli_machinery.standard_logging_options
1726@click.argument(
1727 'service',
1728 metavar=_msg.TranslatedString(_msg.Label.VAULT_METAVAR_SERVICE),
1729 required=False,
1730 default=None,
1731 shell_complete=cli_helpers.shell_complete_service,
1732)
1733@click.pass_context
1734def derivepassphrase_vault(
1735 ctx: click.Context,
1736 /,
1737 **_kwargs: Any, # noqa: ANN401
1738) -> None:
1739 """Derive a passphrase using the vault(1) derivation scheme.
1741 This is a [`click`][CLICK]-powered command-line interface function,
1742 and not intended for programmatic use. See the
1743 derivepassphrase-vault(1) manpage for full documentation of the
1744 interface. (See also [`click.testing.CliRunner`][] for controlled,
1745 programmatic invocation.)
1747 [CLICK]: https://pypi.org/package/click/
1749 Parameters:
1750 ctx (click.Context):
1751 The `click` context.
1753 Other Parameters:
1754 service (str | None):
1755 A service name. Required, unless operating on global
1756 settings or importing/exporting settings.
1757 use_phrase (bool):
1758 Command-line argument `-p`/`--phrase`. If given, query the
1759 user for a passphrase instead of an SSH key.
1760 use_key (bool):
1761 Command-line argument `-k`/`--key`. If given, query the
1762 user for an SSH key instead of a passphrase.
1763 length (int | None):
1764 Command-line argument `-l`/`--length`. Override the default
1765 length of the generated passphrase.
1766 repeat (int | None):
1767 Command-line argument `-r`/`--repeat`. Override the default
1768 repetition limit if positive, or disable the repetition
1769 limit if 0.
1770 lower (int | None):
1771 Command-line argument `--lower`. Require a given amount of
1772 ASCII lowercase characters if positive, else forbid ASCII
1773 lowercase characters if 0.
1774 upper (int | None):
1775 Command-line argument `--upper`. Same as `lower`, but for
1776 ASCII uppercase characters.
1777 number (int | None):
1778 Command-line argument `--number`. Same as `lower`, but for
1779 ASCII digits.
1780 space (int | None):
1781 Command-line argument `--space`. Same as `lower`, but for
1782 the space character.
1783 dash (int | None):
1784 Command-line argument `--dash`. Same as `lower`, but for
1785 the hyphen-minus and underscore characters.
1786 symbol (int | None):
1787 Command-line argument `--symbol`. Same as `lower`, but for
1788 all other ASCII printable characters except lowercase
1789 characters, uppercase characters, digits, space and
1790 backquote.
1791 edit_notes (bool):
1792 Command-line argument `-n`/`--notes`. If given, spawn an
1793 editor to edit notes for `service`.
1794 store_config_only (bool):
1795 Command-line argument `-c`/`--config`. If given, saves the
1796 other given settings (`--key`, ..., `--symbol`) to the
1797 configuration file, either specifically for `service` or as
1798 global settings.
1799 delete_service_settings (bool):
1800 Command-line argument `-x`/`--delete`. If given, removes
1801 the settings for `service` from the configuration file.
1802 delete_globals (bool):
1803 Command-line argument `--delete-globals`. If given, removes
1804 the global settings from the configuration file.
1805 clear_all_settings (bool):
1806 Command-line argument `-X`/`--clear`. If given, removes all
1807 settings from the configuration file.
1808 export_settings (TextIO | os.PathLike[str] | None):
1809 Command-line argument `-e`/`--export`. If a file object,
1810 then it must be open for writing and accept `str` inputs.
1811 Otherwise, a filename to open for writing. Using `-` for
1812 standard output is supported.
1813 import_settings (TextIO | os.PathLike[str] | None):
1814 Command-line argument `-i`/`--import`. If a file object, it
1815 must be open for reading and yield `str` values. Otherwise,
1816 a filename to open for reading. Using `-` for standard
1817 input is supported.
1818 overwrite_config (bool):
1819 Command-line arguments `--overwrite-existing` (True) and
1820 `--merge-existing` (False). Controls whether config saving
1821 and config importing overwrite existing configurations, or
1822 merge them section-wise instead.
1823 unset_settings (Sequence[str]):
1824 Command-line argument `--unset`. If given together with
1825 `--config`, unsets the specified settings (in addition to
1826 any other changes requested).
1827 export_as (Literal['json', 'sh']):
1828 Command-line argument `--export-as`. If given together with
1829 `--export`, selects the format to export the current
1830 configuration as: JSON ("json", default) or POSIX sh ("sh").
1831 modern_editor_interface (bool):
1832 Command-line arguments `--modern-editor-interface` (True)
1833 and `--vault-legacy-editor-interface` (False). Controls
1834 whether editing notes uses a modern editor interface
1835 (supporting comments and aborting) or a vault(1)-compatible
1836 legacy editor interface (WYSIWYG notes contents).
1837 print_notes_before (bool):
1838 Command-line arguments `--print-notes-before` (True) and
1839 `--print-notes-after` (False). Controls whether the service
1840 notes (if any) are printed before the passphrase, or after.
1842 """
1843 vault_context = _VaultContext(ctx) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
1844 vault_context.validate_command_line() 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
1845 vault_context.dispatch_op() 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC
1848if __name__ == '__main__':
1849 derivepassphrase()