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