10c7604bf033455d3e777bdeb028ee7d034a9bb7
Marco Ricci Change the author e-mail ad...

Marco Ricci authored 4 months ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <software@the13thletter.info>
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

2) #
3) # SPDX-License-Identifier: MIT
4) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

5) """Command-line interface for derivepassphrase."""
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

6) 
7) from __future__ import annotations
8) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

9) import base64
10) import collections
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

11) import copy
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

12) import enum
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

13) import importlib
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

14) import inspect
15) import json
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

16) import logging
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

17) import os
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

18) import unicodedata
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

19) from typing import (
20)     TYPE_CHECKING,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

21)     Literal,
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

22)     NoReturn,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

23)     TextIO,
24)     cast,
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

25) )
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

26) 
27) import click
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

28) from typing_extensions import (
29)     Any,
30)     assert_never,
31) )
32) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

33) import derivepassphrase as dpp
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

34) from derivepassphrase import _types, exporter, ssh_agent, vault
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

35) 
36) if TYPE_CHECKING:
37)     import pathlib
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

38)     import socket
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

39)     import types
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

40)     from collections.abc import (
41)         Iterator,
42)         Sequence,
43)     )
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

44) 
45) __author__ = dpp.__author__
46) __version__ = dpp.__version__
47) 
48) __all__ = ('derivepassphrase',)
49) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

50) PROG_NAME = 'derivepassphrase'
Marco Ricci Make suitable SSH key listi...

Marco Ricci authored 1 month ago

51) KEY_DISPLAY_LENGTH = 50
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

52) 
53) # Error messages
54) _INVALID_VAULT_CONFIG = 'Invalid vault config'
55) _AGENT_COMMUNICATION_ERROR = 'Error communicating with the SSH agent'
56) _NO_USABLE_KEYS = 'No usable SSH keys were found'
57) _EMPTY_SELECTION = 'Empty selection'
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

58) 
59) 
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

60) # Top-level
61) # =========
62) 
63) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

64) class _DefaultToVaultGroup(click.Group):
65)     """A helper class to implement the default-to-"vault"-subcommand behavior.
66) 
67)     Modifies internal [`click.MultiCommand`][] methods, and thus is both
68)     an implementation detail and a kludge.
69) 
70)     """
71) 
72)     def resolve_command(
73)         self, ctx: click.Context, args: list[str]
74)     ) -> tuple[str | None, click.Command | None, list[str]]:
75)         """Resolve a command, but default to "vault" instead of erroring out.
76) 
77)         Based on code from click 8.1, which appears to be essentially
78)         untouched since at least click 3.2.
79) 
80)         """
81)         cmd_name = click.utils.make_str(args[0])
82) 
83)         # ORIGINAL COMMENT
84)         # Get the command
85)         cmd = self.get_command(ctx, cmd_name)
86) 
87)         # ORIGINAL COMMENT
88)         # If we can't find the command but there is a normalization
89)         # function available, we try with that one.
90)         if (  # pragma: no cover
91)             cmd is None and ctx.token_normalize_func is not None
92)         ):
93)             cmd_name = ctx.token_normalize_func(cmd_name)
94)             cmd = self.get_command(ctx, cmd_name)
95) 
96)         # ORIGINAL COMMENT
97)         # If we don't find the command we want to show an error message
98)         # to the user that it was not provided.  However, there is
99)         # something else we should do: if the first argument looks like
100)         # an option we want to kick off parsing again for arguments to
101)         # resolve things like --help which now should go to the main
102)         # place.
103)         if cmd is None and not ctx.resilient_parsing:
104)             if click.parser.split_opt(cmd_name)[0]:
105)                 self.parse_args(ctx, ctx.args)
106)             # Instead of calling ctx.fail here, default to "vault", and
107)             # issue a deprecation warning.
108)             click.echo(
109)                 (
110)                     f'{PROG_NAME}: Deprecation warning: A subcommand will be '
111)                     f'required in v1.0. See --help for available subcommands.'
112)                 ),
113)                 err=True,
114)             )
115)             click.echo(
116)                 f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
117)                 err=True,
118)             )
119)             cmd_name = 'vault'
120)             cmd = self.get_command(ctx, cmd_name)
121)             assert cmd is not None, 'Mandatory subcommand "vault" missing!'
122)             args = [cmd_name, *args]
123)         return cmd_name if cmd else None, cmd, args[1:]  # noqa: DOC201
124) 
125) 
126) @click.group(
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

127)     context_settings={
128)         'help_option_names': ['-h', '--help'],
129)         'ignore_unknown_options': True,
130)         'allow_interspersed_args': False,
131)     },
132)     epilog=r"""
133)         Configuration is stored in a directory according to the
134)         DERIVEPASSPHRASE_PATH variable, which defaults to
135)         `~/.derivepassphrase` on UNIX-like systems and
136)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
Marco Ricci Fix minor typo, formatting...

Marco Ricci authored 3 months ago

137)     """,
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

138)     invoke_without_command=True,
139)     cls=_DefaultToVaultGroup,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

140) )
141) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

142) @click.pass_context
143) def derivepassphrase(ctx: click.Context, /) -> None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

144)     """Derive a strong passphrase, deterministically, from a master secret.
145) 
146)     Using a master secret, derive a passphrase for a named service,
147)     subject to constraints e.g. on passphrase length, allowed
148)     characters, etc.  The exact derivation depends on the selected
149)     derivation scheme.  For each scheme, it is computationally
150)     infeasible to discern the master secret from the derived passphrase.
151)     The derivations are also deterministic, given the same inputs, thus
152)     the resulting passphrases need not be stored explicitly.  The
153)     service name and constraints themselves also generally need not be
154)     kept secret, depending on the scheme.
155) 
156)     The currently implemented subcommands are "vault" (for the scheme
157)     used by vault) and "export" (for exporting foreign configuration
158)     data).  See the respective `--help` output for instructions.  If no
159)     subcommand is given, we default to "vault".
160) 
161)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
162)     in v1.0, the subcommand must be specified explicitly.\f
163) 
164)     This is a [`click`][CLICK]-powered command-line interface function,
165)     and not intended for programmatic use.  Call with arguments
166)     `['--help']` to see full documentation of the interface.  (See also
167)     [`click.testing.CliRunner`][] for controlled, programmatic
168)     invocation.)
169) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

170)     [CLICK]: https://pypi.org/package/click/
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

171) 
172)     """  # noqa: D301
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

173)     if ctx.invoked_subcommand is None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

174)         click.echo(
175)             (
176)                 f'{PROG_NAME}: Deprecation warning: A subcommand will be '
177)                 f'required in v1.0. See --help for available subcommands.'
178)             ),
179)             err=True,
180)         )
181)         click.echo(
182)             f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
183)             err=True,
184)         )
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

185)         # See definition of click.Group.invoke, non-chained case.
186)         with ctx:
187)             sub_ctx = derivepassphrase_vault.make_context(
188)                 'vault', ctx.args, parent=ctx
189)             )
190)             with sub_ctx:
191)                 return derivepassphrase_vault.invoke(sub_ctx)
192)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

193) 
194) 
195) # Exporter
196) # ========
197) 
198) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

199) @derivepassphrase.group(
200)     'export',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

201)     context_settings={
202)         'help_option_names': ['-h', '--help'],
203)         'ignore_unknown_options': True,
204)         'allow_interspersed_args': False,
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

205)     },
206)     invoke_without_command=True,
207)     cls=_DefaultToVaultGroup,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

208) )
209) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

210) @click.pass_context
211) def derivepassphrase_export(ctx: click.Context, /) -> None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

212)     """Export a foreign configuration to standard output.
213) 
214)     Read a foreign system configuration, extract all information from
215)     it, and export the resulting configuration to standard output.
216) 
217)     The only available subcommand is "vault", which implements the
218)     vault-native configuration scheme.  If no subcommand is given, we
219)     default to "vault".
220) 
221)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
222)     in v1.0, the subcommand must be specified explicitly.\f
223) 
224)     This is a [`click`][CLICK]-powered command-line interface function,
225)     and not intended for programmatic use.  Call with arguments
226)     `['--help']` to see full documentation of the interface.  (See also
227)     [`click.testing.CliRunner`][] for controlled, programmatic
228)     invocation.)
229) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

230)     [CLICK]: https://pypi.org/package/click/
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

231) 
232)     """  # noqa: D301
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

233)     if ctx.invoked_subcommand is None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

234)         click.echo(
235)             (
236)                 f'{PROG_NAME}: Deprecation warning: A subcommand will be '
237)                 f'required in v1.0. See --help for available subcommands.'
238)             ),
239)             err=True,
240)         )
241)         click.echo(
242)             f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
243)             err=True,
244)         )
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

245)         # See definition of click.Group.invoke, non-chained case.
246)         with ctx:
247)             sub_ctx = derivepassphrase_export_vault.make_context(
248)                 'vault', ctx.args, parent=ctx
249)             )
250)             # Constructing the subcontext above will usually already
251)             # lead to a click.UsageError, so this block typically won't
252)             # actually be called.
253)             with sub_ctx:  # pragma: no cover
254)                 return derivepassphrase_export_vault.invoke(sub_ctx)
255)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

256) 
257) 
258) def _load_data(
259)     fmt: Literal['v0.2', 'v0.3', 'storeroom'],
260)     path: str | bytes | os.PathLike[str],
261)     key: bytes,
262) ) -> Any:  # noqa: ANN401
263)     contents: bytes
264)     module: types.ModuleType
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

265)     # Use match/case here once Python 3.9 becomes unsupported.
266)     if fmt == 'v0.2':
267)         module = importlib.import_module(
268)             'derivepassphrase.exporter.vault_native'
269)         )
270)         if module.STUBBED:
271)             raise ModuleNotFoundError
272)         with open(path, 'rb') as infile:
273)             contents = base64.standard_b64decode(infile.read())
274)         return module.export_vault_native_data(
275)             contents, key, try_formats=['v0.2']
276)         )
277)     elif fmt == 'v0.3':  # noqa: RET505
278)         module = importlib.import_module(
279)             'derivepassphrase.exporter.vault_native'
280)         )
281)         if module.STUBBED:
282)             raise ModuleNotFoundError
283)         with open(path, 'rb') as infile:
284)             contents = base64.standard_b64decode(infile.read())
285)         return module.export_vault_native_data(
286)             contents, key, try_formats=['v0.3']
287)         )
288)     elif fmt == 'storeroom':
289)         module = importlib.import_module('derivepassphrase.exporter.storeroom')
290)         if module.STUBBED:
291)             raise ModuleNotFoundError
292)         return module.export_storeroom_data(path, key)
293)     else:  # pragma: no cover
294)         assert_never(fmt)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

295) 
296) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

297) @derivepassphrase_export.command(
298)     'vault',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

299)     context_settings={'help_option_names': ['-h', '--help']},
300) )
301) @click.option(
302)     '-f',
303)     '--format',
304)     'formats',
305)     metavar='FMT',
306)     multiple=True,
307)     default=('v0.3', 'v0.2', 'storeroom'),
308)     type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
309)     help='try the following storage formats, in order (default: v0.3, v0.2)',
310) )
311) @click.option(
312)     '-k',
313)     '--key',
314)     metavar='K',
315)     help=(
316)         'use K as the storage master key '
317)         '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or '
318)         '`USERNAME` environment variables)'
319)     ),
320) )
321) @click.argument('path', metavar='PATH', required=True)
322) @click.pass_context
323) def derivepassphrase_export_vault(
324)     ctx: click.Context,
325)     /,
326)     *,
327)     path: str | bytes | os.PathLike[str],
328)     formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
329)     key: str | bytes | None = None,
330) ) -> None:
331)     """Export a vault-native configuration to standard output.
332) 
333)     Read the vault-native configuration at PATH, extract all information
334)     from it, and export the resulting configuration to standard output.
335)     Depending on the configuration format, PATH may either be a file or
336)     a directory.  Supports the vault "v0.2", "v0.3" and "storeroom"
337)     formats.
338) 
339)     If PATH is explicitly given as `VAULT_PATH`, then use the
340)     `VAULT_PATH` environment variable to determine the correct path.
341)     (Use `./VAULT_PATH` or similar to indicate a file/directory actually
342)     named `VAULT_PATH`.)
343) 
344)     """
345)     logging.basicConfig()
346)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
347)         path = exporter.get_vault_path()
348)     if key is None:
349)         key = exporter.get_vault_key()
350)     elif isinstance(key, str):  # pragma: no branch
351)         key = key.encode('utf-8')
352)     for fmt in formats:
353)         try:
354)             config = _load_data(fmt, path, key)
355)         except (
356)             IsADirectoryError,
357)             NotADirectoryError,
358)             ValueError,
359)             RuntimeError,
360)         ):
361)             logging.info('Cannot load as %s: %s', fmt, path)
362)             continue
363)         except OSError as exc:
364)             click.echo(
365)                 (
366)                     f'{PROG_NAME}: ERROR: Cannot parse {path!r} as '
367)                     f'a valid config: {exc.strerror}: {exc.filename!r}'
368)                 ),
369)                 err=True,
370)             )
371)             ctx.exit(1)
372)         except ModuleNotFoundError:
373)             # TODO(the-13th-letter): Use backslash continuation.
374)             # https://github.com/nedbat/coveragepy/issues/1836
375)             msg = f"""
376) {PROG_NAME}: ERROR: Cannot load the required Python module "cryptography".
377) {PROG_NAME}: INFO: pip users: see the "export" extra.
378) """.lstrip('\n')
379)             click.echo(msg, nl=False, err=True)
380)             ctx.exit(1)
381)         else:
382)             if not _types.is_vault_config(config):
383)                 click.echo(
384)                     f'{PROG_NAME}: ERROR: Invalid vault config: {config!r}',
385)                     err=True,
386)                 )
387)                 ctx.exit(1)
388)             click.echo(json.dumps(config, indent=2, sort_keys=True))
389)             break
390)     else:
391)         click.echo(
392)             f'{PROG_NAME}: ERROR: Cannot parse {path!r} as a valid config.',
393)             err=True,
394)         )
395)         ctx.exit(1)
396) 
397) 
398) # Vault
399) # =====
400) 
401) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

402) def _config_filename(
403)     subsystem: str | None = 'settings',
404) ) -> str | bytes | pathlib.Path:
405)     """Return the filename of the configuration file for the subsystem.
406) 
407)     The (implicit default) file is currently named `settings.json`,
408)     located within the configuration directory as determined by the
409)     `DERIVEPASSPHRASE_PATH` environment variable, or by
410)     [`click.get_app_dir`][] in POSIX mode.  Depending on the requested
411)     subsystem, this will usually be a different file within that
412)     directory.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

413) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

414)     Args:
415)         subsystem:
416)             Name of the configuration subsystem whose configuration
417)             filename to return.  If not given, return the old filename
418)             from before the subcommand migration.  If `None`, return the
419)             configuration directory instead.
420) 
421)     Raises:
422)         AssertionError:
423)             An unknown subsystem was passed.
424) 
425)     Deprecated:
426)         Since v0.2.0: The implicit default subsystem and the old
427)         configuration filename are deprecated, and will be removed in v1.0.
428)         The subsystem will be mandatory to specify.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

429) 
430)     """
431)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

432)     path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir(
433)         PROG_NAME, force_posix=True
434)     )
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

435)     # Use match/case here once Python 3.9 becomes unsupported.
436)     if subsystem is None:
437)         return path
438)     elif subsystem in {'vault', 'settings'}:  # noqa: RET505
439)         filename = f'{subsystem}.json'
440)     else:  # pragma: no cover
441)         msg = f'Unknown configuration subsystem: {subsystem!r}'
442)         raise AssertionError(msg)
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

443)     return os.path.join(path, filename)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

444) 
445) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

446) def _load_config() -> _types.VaultConfig:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

447)     """Load a vault(1)-compatible config from the application directory.
448) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

449)     The filename is obtained via [`_config_filename`][].  This must be
450)     an unencrypted JSON file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

451) 
452)     Returns:
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

453)         The vault settings.  See [`_types.VaultConfig`][] for details.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

454) 
455)     Raises:
456)         OSError:
457)             There was an OS error accessing the file.
458)         ValueError:
459)             The data loaded from the file is not a vault(1)-compatible
460)             config.
461) 
462)     """
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

463)     filename = _config_filename(subsystem='vault')
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

464)     with open(filename, 'rb') as fileobj:
465)         data = json.load(fileobj)
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

466)     if not _types.is_vault_config(data):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

467)         raise ValueError(_INVALID_VAULT_CONFIG)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

468)     return data
469) 
470) 
Marco Ricci Permit one flaky test and f...

Marco Ricci authored 3 months ago

471) def _migrate_and_load_old_config() -> tuple[
472)     _types.VaultConfig, OSError | None
473) ]:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

474)     """Load and migrate a vault(1)-compatible config.
475) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

476)     The (old) filename is obtained via [`_config_filename`][].  This
477)     must be an unencrypted JSON file.  After loading, the file is
478)     migrated to the new standard filename.
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

479) 
480)     Returns:
481)         The vault settings, and an optional exception encountered during
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

482)         migration.  See [`_types.VaultConfig`][] for details on the
483)         former.
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

484) 
485)     Raises:
486)         OSError:
487)             There was an OS error accessing the old file.
488)         ValueError:
489)             The data loaded from the file is not a vault(1)-compatible
490)             config.
491) 
492)     """
493)     new_filename = _config_filename(subsystem='vault')
494)     old_filename = _config_filename()
495)     with open(old_filename, 'rb') as fileobj:
496)         data = json.load(fileobj)
497)     if not _types.is_vault_config(data):
498)         raise ValueError(_INVALID_VAULT_CONFIG)
499)     try:
500)         os.replace(old_filename, new_filename)
501)     except OSError as exc:
502)         return data, exc
503)     else:
504)         return data, None
505) 
506) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

507) def _save_config(config: _types.VaultConfig, /) -> None:
Marco Ricci Create the configuration di...

Marco Ricci authored 5 months ago

508)     """Save a vault(1)-compatible config to the application directory.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

509) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

510)     The filename is obtained via [`_config_filename`][].  The config
511)     will be stored as an unencrypted JSON file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

512) 
513)     Args:
514)         config:
515)             vault configuration to save.
516) 
517)     Raises:
518)         OSError:
519)             There was an OS error accessing or writing the file.
520)         ValueError:
521)             The data cannot be stored as a vault(1)-compatible config.
522) 
523)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

524)     if not _types.is_vault_config(config):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

525)         raise ValueError(_INVALID_VAULT_CONFIG)
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

526)     filename = _config_filename(subsystem='vault')
Marco Ricci Create the configuration di...

Marco Ricci authored 5 months ago

527)     filedir = os.path.dirname(os.path.abspath(filename))
528)     try:
529)         os.makedirs(filedir, exist_ok=False)
530)     except FileExistsError:
531)         if not os.path.isdir(filedir):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

532)             raise  # noqa: DOC501
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

533)     with open(filename, 'w', encoding='UTF-8') as fileobj:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

534)         json.dump(config, fileobj)
535) 
536) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

537) def _get_suitable_ssh_keys(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

538)     conn: ssh_agent.SSHAgentClient | socket.socket | None = None, /
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

539) ) -> Iterator[_types.KeyCommentPair]:
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

540)     """Yield all SSH keys suitable for passphrase derivation.
541) 
542)     Suitable SSH keys are queried from the running SSH agent (see
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

543)     [`ssh_agent.SSHAgentClient.list_keys`][]).
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

544) 
545)     Args:
546)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

547)             An optional connection hint to the SSH agent.  See
548)             [`ssh_agent.SSHAgentClient.ensure_agent_subcontext`][].
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

549) 
550)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 3 months ago

551)         Every SSH key from the SSH agent that is suitable for passphrase
552)         derivation.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

553) 
554)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

555)         KeyError:
556)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
557)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

558)         NotImplementedError:
559)             `conn` was `None`, and this Python does not support
560)             [`socket.AF_UNIX`][], so the SSH agent client cannot be
561)             automatically set up.
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

562)         OSError:
563)             `conn` was a socket or `None`, and there was an error
564)             setting up a socket connection to the agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 6 months ago

565)         LookupError:
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

566)             No keys usable for passphrase derivation are loaded into the
567)             SSH agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 6 months ago

568)         RuntimeError:
569)             There was an error communicating with the SSH agent.
Marco Ricci Fix miscellaneous small doc...

Marco Ricci authored 3 months ago

570)         ssh_agent.SSHAgentFailedError:
Marco Ricci Add a specific error class...

Marco Ricci authored 4 months ago

571)             The agent failed to supply a list of loaded keys.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

572) 
573)     """
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

574)     with ssh_agent.SSHAgentClient.ensure_agent_subcontext(conn) as client:
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

575)         try:
576)             all_key_comment_pairs = list(client.list_keys())
577)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

578)             raise RuntimeError(_AGENT_COMMUNICATION_ERROR) from e
Marco Ricci Publish polished `is_suitab...

Marco Ricci authored 1 month ago

579)         suitable_keys = copy.copy(all_key_comment_pairs)
580)         for pair in all_key_comment_pairs:
581)             key, _comment = pair
582)             if vault.Vault.is_suitable_ssh_key(key, client=client):
583)                 yield pair
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

584)     if not suitable_keys:  # pragma: no cover
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

585)         raise LookupError(_NO_USABLE_KEYS)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

586) 
587) 
588) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

589)     items: Sequence[str | bytes],
590)     heading: str = 'Possible choices:',
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

591)     single_choice_prompt: str = 'Confirm this choice?',
592) ) -> int:
593)     """Prompt user for a choice among the given items.
594) 
595)     Print the heading, if any, then present the items to the user.  If
596)     there are multiple items, prompt the user for a selection, validate
597)     the choice, then return the list index of the selected item.  If
598)     there is only a single item, request confirmation for that item
599)     instead, and return the correct index.
600) 
601)     Args:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

602)         items:
603)             The list of items to choose from.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

604)         heading:
605)             A heading for the list of items, to print immediately
606)             before.  Defaults to a reasonable standard heading.  If
607)             explicitly empty, print no heading.
608)         single_choice_prompt:
609)             The confirmation prompt if there is only a single possible
610)             choice.  Defaults to a reasonable standard prompt.
611) 
612)     Returns:
613)         An index into the items sequence, indicating the user's
614)         selection.
615) 
616)     Raises:
617)         IndexError:
618)             The user made an invalid or empty selection, or requested an
619)             abort.
620) 
621)     """
622)     n = len(items)
623)     if heading:
624)         click.echo(click.style(heading, bold=True))
625)     for i, x in enumerate(items, start=1):
626)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
627)         click.echo(' ', nl=False)
628)         click.echo(x)
629)     if n > 1:
630)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
631)         choice = click.prompt(
632)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

633)             err=True,
634)             type=choices,
635)             show_choices=False,
636)             show_default=False,
637)             default='',
638)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

639)         if not choice:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

640)             raise IndexError(_EMPTY_SELECTION)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

641)         return int(choice) - 1
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

642)     prompt_suffix = (
643)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
644)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

645)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

646)         click.confirm(
647)             single_choice_prompt,
648)             prompt_suffix=prompt_suffix,
649)             err=True,
650)             abort=True,
651)             default=False,
652)             show_default=False,
653)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

654)     except click.Abort:
655)         raise IndexError(_EMPTY_SELECTION) from None
656)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

657) 
658) 
659) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

660)     conn: ssh_agent.SSHAgentClient | socket.socket | None = None, /
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

661) ) -> bytes | bytearray:
662)     """Interactively select an SSH key for passphrase derivation.
663) 
664)     Suitable SSH keys are queried from the running SSH agent (see
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

665)     [`ssh_agent.SSHAgentClient.list_keys`][]), then the user is prompted
666)     interactively (see [`click.prompt`][]) for a selection.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

667) 
668)     Args:
669)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

670)             An optional connection hint to the SSH agent.  See
671)             [`ssh_agent.SSHAgentClient.ensure_agent_subcontext`][].
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

672) 
673)     Returns:
674)         The selected SSH key.
675) 
676)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

677)         KeyError:
678)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
679)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

680)         NotImplementedError:
681)             `conn` was `None`, and this Python does not support
682)             [`socket.AF_UNIX`][], so the SSH agent client cannot be
683)             automatically set up.
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

684)         OSError:
685)             `conn` was a socket or `None`, and there was an error
686)             setting up a socket connection to the agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

687)         IndexError:
688)             The user made an invalid or empty selection, or requested an
689)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 6 months ago

690)         LookupError:
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

691)             No keys usable for passphrase derivation are loaded into the
692)             SSH agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 6 months ago

693)         RuntimeError:
694)             There was an error communicating with the SSH agent.
Marco Ricci Add a specific error class...

Marco Ricci authored 4 months ago

695)         SSHAgentFailedError:
696)             The agent failed to supply a list of loaded keys.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

697)     """
698)     suitable_keys = list(_get_suitable_ssh_keys(conn))
699)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

700)     unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

701)     for key, comment in suitable_keys:
702)         keytype = unstring_prefix(key)[0].decode('ASCII')
703)         key_str = base64.standard_b64encode(key).decode('ASCII')
Marco Ricci Make suitable SSH key listi...

Marco Ricci authored 1 month ago

704)         remaining_key_display_length = KEY_DISPLAY_LENGTH - 1 - len(keytype)
705)         key_extract = min(
706)             key_str,
707)             '...' + key_str[-remaining_key_display_length:],
708)             key=len,
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

709)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

710)         comment_str = comment.decode('UTF-8', errors='replace')
Marco Ricci Make suitable SSH key listi...

Marco Ricci authored 1 month ago

711)         key_listing.append(f'{keytype} {key_extract}  {comment_str}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

712)     choice = _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

713)         key_listing,
714)         heading='Suitable SSH keys:',
715)         single_choice_prompt='Use this key?',
716)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

717)     return suitable_keys[choice].key
718) 
719) 
720) def _prompt_for_passphrase() -> str:
721)     """Interactively prompt for the passphrase.
722) 
723)     Calls [`click.prompt`][] internally.  Moved into a separate function
724)     mainly for testing/mocking purposes.
725) 
726)     Returns:
727)         The user input.
728) 
729)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

730)     return cast(
731)         str,
732)         click.prompt(
733)             'Passphrase',
734)             default='',
735)             hide_input=True,
736)             show_default=False,
737)             err=True,
738)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

739)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

740) 
741) 
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

742) class _ORIGIN(enum.Enum):
743)     INTERACTIVE: str = 'interactive'
744) 
745) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

746) def _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

747)     key: tuple[str, ...] | _ORIGIN,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

748)     value: dict[str, Any],
749)     *,
750)     form: Literal['NFC', 'NFD', 'NFKC', 'NFKD'] = 'NFC',
751) ) -> None:
752)     if 'phrase' in value:
753)         phrase = value['phrase']
754)         if not unicodedata.is_normalized(form, phrase):
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

755)             formatted_key = (
756)                 key.value
757)                 if isinstance(key, _ORIGIN)
758)                 else _types.json_path(key)
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

759)             )
760)             click.echo(
761)                 (
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

762)                     f'{PROG_NAME}: Warning: the {formatted_key} '
763)                     f'passphrase is not {form}-normalized. Make sure to '
764)                     f'double-check this is really the passphrase you want.'
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

765)                 ),
766)                 err=True,
767)             )
768) 
769) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

770) class OptionGroupOption(click.Option):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

771)     """A [`click.Option`][] with an associated group name and group epilog.
772) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

773)     Used by [`CommandWithHelpGroups`][] to print help sections.  Each
774)     subclass contains its own group name and epilog.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

775) 
776)     Attributes:
777)         option_group_name:
778)             The name of the option group.  Used as a heading on the help
779)             text for options in this section.
780)         epilog:
781)             An epilog to print after listing the options in this
782)             section.
783) 
784)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

785) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

786)     option_group_name: str = ''
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

787)     """"""
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

788)     epilog: str = ''
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

789)     """"""
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

790) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

791)     def __init__(self, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

792)         if self.__class__ == __class__:  # type: ignore[name-defined]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

793)             raise NotImplementedError
794)         super().__init__(*args, **kwargs)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

795) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

796) 
797) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

798)     """A [`click.Command`][] with support for help/option groups.
799) 
800)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
801)     further modified to support group epilogs.
802) 
803)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
804) 
805)     """
806) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

807)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

808)         self,
809)         ctx: click.Context,
810)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

811)     ) -> None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

812)         r"""Format options on the help listing, grouped into sections.
813) 
Marco Ricci Add minor documentation rew...

Marco Ricci authored 6 months ago

814)         This is a callback for [`click.Command.get_help`][] that
815)         implements the `--help` listing, by calling appropriate methods
816)         of the `formatter`.  We list all options (like the base
817)         implementation), but grouped into sections according to the
818)         concrete [`click.Option`][] subclass being used.  If the option
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

819)         is an instance of some subclass of [`OptionGroupOption`][], then
820)         the section heading and the epilog are taken from the
821)         [`option_group_name`] [OptionGroupOption.option_group_name] and
822)         [`epilog`] [OptionGroupOption.epilog] attributes; otherwise, the
823)         section heading is "Options" (or "Other options" if there are
824)         other option groups) and the epilog is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

825) 
826)         Args:
827)             ctx:
828)                 The click context.
829)             formatter:
830)                 The formatter for the `--help` listing.
831) 
832)         """
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

833)         help_records: dict[str, list[tuple[str, str]]] = {}
834)         epilogs: dict[str, str] = {}
835)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

836)         if (  # pragma: no branch
837)             (help_opt := self.get_help_option(ctx)) is not None
838)             and help_opt not in params
839)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

840)             params.append(help_opt)
841)         for param in params:
842)             rec = param.get_help_record(ctx)
843)             if rec is not None:
844)                 if isinstance(param, OptionGroupOption):
845)                     group_name = param.option_group_name
846)                     epilogs.setdefault(group_name, param.epilog)
847)                 else:
848)                     group_name = ''
849)                 help_records.setdefault(group_name, []).append(rec)
850)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

851)         default_group_name = (
852)             'Other Options' if len(default_group) > 1 else 'Options'
853)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

854)         help_records[default_group_name] = default_group
855)         for group_name, records in help_records.items():
856)             with formatter.section(group_name):
857)                 formatter.write_dl(records)
858)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
859)             if epilog:
860)                 formatter.write_paragraph()
861)                 with formatter.indentation():
862)                     formatter.write_text(epilog)
863) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

864) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

865) # Concrete option groups used by this command-line interface.
866) class PasswordGenerationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

867)     """Password generation options for the CLI."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

868) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

869)     option_group_name = 'Password generation'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

870)     epilog = """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

871)         Use NUMBER=0, e.g. "--symbol 0", to exclude a character type
872)         from the output.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

873)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

874) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

875) 
876) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

877)     """Configuration options for the CLI."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

878) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

879)     option_group_name = 'Configuration'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

880)     epilog = """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

881)         Use $VISUAL or $EDITOR to configure the spawned editor.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

882)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

883) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

884) 
885) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

886)     """Storage management options for the CLI."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

887) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

888)     option_group_name = 'Storage management'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

889)     epilog = """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

890)         Using "-" as PATH for standard input/standard output is
891)         supported.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

892)     """
893) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

894) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

895) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

896)     ctx: click.Context,
897)     param: click.Parameter,
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

898)     value: Any,  # noqa: ANN401
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

899) ) -> int | None:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

900)     """Check that the occurrence constraint is valid (int, 0 or larger).
901) 
902)     Args:
903)         ctx: The `click` context.
904)         param: The current command-line parameter.
905)         value: The parameter value to be checked.
906) 
907)     Returns:
908)         The parsed parameter value.
909) 
910)     Raises:
911)         click.BadParameter: The parameter value is invalid.
912) 
913)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

914)     del ctx  # Unused.
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

915)     del param  # Unused.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

916)     if value is None:
917)         return value
918)     if isinstance(value, int):
919)         int_value = value
920)     else:
921)         try:
922)             int_value = int(value, 10)
923)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

924)             msg = 'not an integer'
925)             raise click.BadParameter(msg) from e
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

926)     if int_value < 0:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

927)         msg = 'not a non-negative integer'
928)         raise click.BadParameter(msg)
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

929)     return int_value
930) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

931) 
932) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

933)     ctx: click.Context,
934)     param: click.Parameter,
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

935)     value: Any,  # noqa: ANN401
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

936) ) -> int | None:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

937)     """Check that the length is valid (int, 1 or larger).
938) 
939)     Args:
940)         ctx: The `click` context.
941)         param: The current command-line parameter.
942)         value: The parameter value to be checked.
943) 
944)     Returns:
945)         The parsed parameter value.
946) 
947)     Raises:
948)         click.BadParameter: The parameter value is invalid.
949) 
950)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

951)     del ctx  # Unused.
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

952)     del param  # Unused.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

953)     if value is None:
954)         return value
955)     if isinstance(value, int):
956)         int_value = value
957)     else:
958)         try:
959)             int_value = int(value, 10)
960)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

961)             msg = 'not an integer'
962)             raise click.BadParameter(msg) from e
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

963)     if int_value < 1:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

964)         msg = 'not a positive integer'
965)         raise click.BadParameter(msg)
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

966)     return int_value
967) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

968) 
969) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

970) # Enter notes below the line with the cut mark (ASCII scissors and
971) # dashes).  Lines above the cut mark (such as this one) will be ignored.
972) #
973) # If you wish to clear the notes, leave everything beyond the cut mark
974) # blank.  However, if you leave the *entire* file blank, also removing
975) # the cut mark, then the edit is aborted, and the old notes contents are
976) # retained.
977) #
978) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

979) """
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

980) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
981) 
982) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

983) @derivepassphrase.command(
984)     'vault',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

985)     context_settings={'help_option_names': ['-h', '--help']},
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

986)     cls=CommandWithHelpGroups,
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

987)     epilog=r"""
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

988)         WARNING: There is NO WAY to retrieve the generated passphrases
989)         if the master passphrase, the SSH key, or the exact passphrase
990)         settings are lost, short of trying out all possible
991)         combinations.  You are STRONGLY advised to keep independent
992)         backups of the settings and the SSH key, if any.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

993) 
994)         The configuration is NOT encrypted, and you are STRONGLY
995)         discouraged from using a stored passphrase.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

996)     """,
997) )
998) @click.option(
999)     '-p',
1000)     '--phrase',
1001)     'use_phrase',
1002)     is_flag=True,
1003)     help='prompts you for your passphrase',
1004)     cls=PasswordGenerationOption,
1005) )
1006) @click.option(
1007)     '-k',
1008)     '--key',
1009)     'use_key',
1010)     is_flag=True,
1011)     help='uses your SSH private key to generate passwords',
1012)     cls=PasswordGenerationOption,
1013) )
1014) @click.option(
1015)     '-l',
1016)     '--length',
1017)     metavar='NUMBER',
1018)     callback=_validate_length,
1019)     help='emits password of length NUMBER',
1020)     cls=PasswordGenerationOption,
1021) )
1022) @click.option(
1023)     '-r',
1024)     '--repeat',
1025)     metavar='NUMBER',
1026)     callback=_validate_occurrence_constraint,
1027)     help='allows maximum of NUMBER repeated adjacent chars',
1028)     cls=PasswordGenerationOption,
1029) )
1030) @click.option(
1031)     '--lower',
1032)     metavar='NUMBER',
1033)     callback=_validate_occurrence_constraint,
1034)     help='includes at least NUMBER lowercase letters',
1035)     cls=PasswordGenerationOption,
1036) )
1037) @click.option(
1038)     '--upper',
1039)     metavar='NUMBER',
1040)     callback=_validate_occurrence_constraint,
1041)     help='includes at least NUMBER uppercase letters',
1042)     cls=PasswordGenerationOption,
1043) )
1044) @click.option(
1045)     '--number',
1046)     metavar='NUMBER',
1047)     callback=_validate_occurrence_constraint,
1048)     help='includes at least NUMBER digits',
1049)     cls=PasswordGenerationOption,
1050) )
1051) @click.option(
1052)     '--space',
1053)     metavar='NUMBER',
1054)     callback=_validate_occurrence_constraint,
1055)     help='includes at least NUMBER spaces',
1056)     cls=PasswordGenerationOption,
1057) )
1058) @click.option(
1059)     '--dash',
1060)     metavar='NUMBER',
1061)     callback=_validate_occurrence_constraint,
1062)     help='includes at least NUMBER "-" or "_"',
1063)     cls=PasswordGenerationOption,
1064) )
1065) @click.option(
1066)     '--symbol',
1067)     metavar='NUMBER',
1068)     callback=_validate_occurrence_constraint,
1069)     help='includes at least NUMBER symbol chars',
1070)     cls=PasswordGenerationOption,
1071) )
1072) @click.option(
1073)     '-n',
1074)     '--notes',
1075)     'edit_notes',
1076)     is_flag=True,
1077)     help='spawn an editor to edit notes for SERVICE',
1078)     cls=ConfigurationOption,
1079) )
1080) @click.option(
1081)     '-c',
1082)     '--config',
1083)     'store_config_only',
1084)     is_flag=True,
1085)     help='saves the given settings for SERVICE or global',
1086)     cls=ConfigurationOption,
1087) )
1088) @click.option(
1089)     '-x',
1090)     '--delete',
1091)     'delete_service_settings',
1092)     is_flag=True,
1093)     help='deletes settings for SERVICE',
1094)     cls=ConfigurationOption,
1095) )
1096) @click.option(
1097)     '--delete-globals',
1098)     is_flag=True,
1099)     help='deletes the global shared settings',
1100)     cls=ConfigurationOption,
1101) )
1102) @click.option(
1103)     '-X',
1104)     '--clear',
1105)     'clear_all_settings',
1106)     is_flag=True,
1107)     help='deletes all settings',
1108)     cls=ConfigurationOption,
1109) )
1110) @click.option(
1111)     '-e',
1112)     '--export',
1113)     'export_settings',
1114)     metavar='PATH',
1115)     help='export all saved settings into file PATH',
1116)     cls=StorageManagementOption,
1117) )
1118) @click.option(
1119)     '-i',
1120)     '--import',
1121)     'import_settings',
1122)     metavar='PATH',
1123)     help='import saved settings from file PATH',
1124)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1125) )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1126) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1127) @click.argument('service', required=False)
1128) @click.pass_context
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

1129) def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1130)     ctx: click.Context,
1131)     /,
1132)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1133)     service: str | None = None,
1134)     use_phrase: bool = False,
1135)     use_key: bool = False,
1136)     length: int | None = None,
1137)     repeat: int | None = None,
1138)     lower: int | None = None,
1139)     upper: int | None = None,
1140)     number: int | None = None,
1141)     space: int | None = None,
1142)     dash: int | None = None,
1143)     symbol: int | None = None,
1144)     edit_notes: bool = False,
1145)     store_config_only: bool = False,
1146)     delete_service_settings: bool = False,
1147)     delete_globals: bool = False,
1148)     clear_all_settings: bool = False,
1149)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
1150)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1151) ) -> None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

1152)     """Derive a passphrase using the vault(1) derivation scheme.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1153) 
Marco Ricci Fill out README and documen...

Marco Ricci authored 6 months ago

1154)     Using a master passphrase or a master SSH key, derive a passphrase
1155)     for SERVICE, subject to length, character and character repetition
1156)     constraints.  The derivation is cryptographically strong, meaning
1157)     that even if a single passphrase is compromised, guessing the master
1158)     passphrase or a different service's passphrase is computationally
1159)     infeasible.  The derivation is also deterministic, given the same
1160)     inputs, thus the resulting passphrase need not be stored explicitly.
1161)     The service name and constraints themselves also need not be kept
1162)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1163) 
1164)     If operating on global settings, or importing/exporting settings,
1165)     then SERVICE must be omitted.  Otherwise it is required.\f
1166) 
1167)     This is a [`click`][CLICK]-powered command-line interface function,
1168)     and not intended for programmatic use.  Call with arguments
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1169)     `['--help']` to see full documentation of the interface.  (See also
1170)     [`click.testing.CliRunner`][] for controlled, programmatic
1171)     invocation.)
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1172) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

1173)     [CLICK]: https://pypi.org/package/click/
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1174) 
1175)     Parameters:
1176)         ctx (click.Context):
1177)             The `click` context.
1178) 
1179)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1180)         service:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1181)             A service name.  Required, unless operating on global
1182)             settings or importing/exporting settings.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1183)         use_phrase:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1184)             Command-line argument `-p`/`--phrase`.  If given, query the
1185)             user for a passphrase instead of an SSH key.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1186)         use_key:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1187)             Command-line argument `-k`/`--key`.  If given, query the
1188)             user for an SSH key instead of a passphrase.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1189)         length:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1190)             Command-line argument `-l`/`--length`.  Override the default
1191)             length of the generated passphrase.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1192)         repeat:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1193)             Command-line argument `-r`/`--repeat`.  Override the default
1194)             repetition limit if positive, or disable the repetition
1195)             limit if 0.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1196)         lower:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1197)             Command-line argument `--lower`.  Require a given amount of
1198)             ASCII lowercase characters if positive, else forbid ASCII
1199)             lowercase characters if 0.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1200)         upper:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1201)             Command-line argument `--upper`.  Same as `lower`, but for
1202)             ASCII uppercase characters.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1203)         number:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1204)             Command-line argument `--number`.  Same as `lower`, but for
1205)             ASCII digits.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1206)         space:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

1207)             Command-line argument `--space`.  Same as `lower`, but for
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1208)             the space character.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1209)         dash:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

1210)             Command-line argument `--dash`.  Same as `lower`, but for
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1211)             the hyphen-minus and underscore characters.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1212)         symbol:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

1213)             Command-line argument `--symbol`.  Same as `lower`, but for
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1214)             all other ASCII printable characters (except backquote).
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1215)         edit_notes:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1216)             Command-line argument `-n`/`--notes`.  If given, spawn an
1217)             editor to edit notes for `service`.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1218)         store_config_only:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1219)             Command-line argument `-c`/`--config`.  If given, saves the
1220)             other given settings (`--key`, ..., `--symbol`) to the
1221)             configuration file, either specifically for `service` or as
1222)             global settings.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1223)         delete_service_settings:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1224)             Command-line argument `-x`/`--delete`.  If given, removes
1225)             the settings for `service` from the configuration file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1226)         delete_globals:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1227)             Command-line argument `--delete-globals`.  If given, removes
1228)             the global settings from the configuration file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1229)         clear_all_settings:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1230)             Command-line argument `-X`/`--clear`.  If given, removes all
1231)             settings from the configuration file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1232)         export_settings:
1233)             Command-line argument `-e`/`--export`.  If a file object,
1234)             then it must be open for writing and accept `str` inputs.
1235)             Otherwise, a filename to open for writing.  Using `-` for
1236)             standard output is supported.
1237)         import_settings:
1238)             Command-line argument `-i`/`--import`.  If a file object, it
1239)             must be open for reading and yield `str` values.  Otherwise,
1240)             a filename to open for reading.  Using `-` for standard
1241)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1242) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

1243)     """  # noqa: D301
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1244)     options_in_group: dict[type[click.Option], list[click.Option]] = {}
1245)     params_by_str: dict[str, click.Parameter] = {}
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1246)     for param in ctx.command.params:
1247)         if isinstance(param, click.Option):
1248)             group: type[click.Option]
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

1249)             # Use match/case here once Python 3.9 becomes unsupported.
1250)             if isinstance(param, PasswordGenerationOption):
1251)                 group = PasswordGenerationOption
1252)             elif isinstance(param, ConfigurationOption):
1253)                 group = ConfigurationOption
1254)             elif isinstance(param, StorageManagementOption):
1255)                 group = StorageManagementOption
1256)             elif isinstance(param, OptionGroupOption):
1257)                 raise AssertionError(  # noqa: DOC501,TRY003,TRY004
1258)                     f'Unknown option group for {param!r}'  # noqa: EM102
1259)                 )
1260)             else:
1261)                 group = click.Option
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1262)             options_in_group.setdefault(group, []).append(param)
1263)         params_by_str[param.human_readable_name] = param
1264)         for name in param.opts + param.secondary_opts:
1265)             params_by_str[name] = param
1266) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

1267)     def is_param_set(param: click.Parameter) -> bool:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1268)         return bool(ctx.params.get(param.human_readable_name))
1269) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1270)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1271)         param: click.Parameter | str,
1272)         *incompatible: click.Parameter | str,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1273)     ) -> None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1274)         if isinstance(param, str):
1275)             param = params_by_str[param]
1276)         assert isinstance(param, click.Parameter)
1277)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1278)             return
1279)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1280)             if isinstance(other, str):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1281)                 other = params_by_str[other]  # noqa: PLW2901
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1282)             assert isinstance(other, click.Parameter)
1283)             if other != param and is_param_set(other):
1284)                 opt_str = param.opts[0]
1285)                 other_str = other.opts[0]
1286)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1287)                     opt_str, f'mutually exclusive with {other_str}', ctx=ctx
1288)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1289) 
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1290)     def err(msg: str) -> NoReturn:
1291)         click.echo(f'{PROG_NAME}: {msg}', err=True)
1292)         ctx.exit(1)
1293) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

1294)     def get_config() -> _types.VaultConfig:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1295)         try:
1296)             return _load_config()
1297)         except FileNotFoundError:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1298)             try:
1299)                 backup_config, exc = _migrate_and_load_old_config()
1300)             except FileNotFoundError:
1301)                 return {'services': {}}
1302)             old_name = os.path.basename(_config_filename())
1303)             new_name = os.path.basename(_config_filename(subsystem='vault'))
1304)             click.echo(
1305)                 (
1306)                     f'{PROG_NAME}: Using deprecated v0.1-style config file '
1307)                     f'{old_name!r}, instead of v0.2-style {new_name!r}.  '
1308)                     f'Support for v0.1-style config filenames will be '
1309)                     f'removed in v1.0.'
1310)                 ),
1311)                 err=True,
1312)             )
1313)             if isinstance(exc, OSError):
1314)                 click.echo(
1315)                     (
1316)                         f'{PROG_NAME}: Warning: Failed to migrate to '
1317)                         f'{new_name!r}: {exc.strerror}: {exc.filename!r}'
1318)                     ),
1319)                     err=True,
1320)                 )
1321)             else:
1322)                 click.echo(
1323)                     f'{PROG_NAME}: Successfully migrated to {new_name!r}.',
1324)                     err=True,
1325)                 )
1326)             return backup_config
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1327)         except OSError as e:
1328)             err(f'Cannot load config: {e.strerror}: {e.filename!r}')
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1329)         except Exception as e:  # noqa: BLE001
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1330)             err(f'Cannot load config: {e}')
1331) 
1332)     def put_config(config: _types.VaultConfig, /) -> None:
1333)         try:
1334)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1335)         except OSError as exc:
1336)             err(f'Cannot store config: {exc.strerror}: {exc.filename!r}')
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1337)         except Exception as exc:  # noqa: BLE001
1338)             err(f'Cannot store config: {exc}')
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1339) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

1340)     configuration: _types.VaultConfig
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1341) 
1342)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1343)     for group in (ConfigurationOption, StorageManagementOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1344)         for opt in options_in_group[group]:
1345)             if opt != params_by_str['--config']:
1346)                 check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1347)                     opt, *options_in_group[PasswordGenerationOption]
1348)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1349) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1350)     for group in (ConfigurationOption, StorageManagementOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1351)         for opt in options_in_group[group]:
1352)             check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1353)                 opt,
1354)                 *options_in_group[ConfigurationOption],
1355)                 *options_in_group[StorageManagementOption],
1356)             )
Marco Ricci Correctly model vault globa...

Marco Ricci authored 2 months ago

1357)     sv_or_global_options = options_in_group[PasswordGenerationOption]
1358)     for param in sv_or_global_options:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1359)         if is_param_set(param) and not (
1360)             service or is_param_set(params_by_str['--config'])
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1361)         ):
1362)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1363)             msg = f'{opt_str} requires a SERVICE or --config'
Marco Ricci Correctly model vault globa...

Marco Ricci authored 2 months ago

1364)             raise click.UsageError(msg)  # noqa: DOC501
1365)     sv_options = [params_by_str['--notes'], params_by_str['--delete']]
1366)     for param in sv_options:
1367)         if is_param_set(param) and not service:
1368)             opt_str = param.opts[0]
1369)             msg = f'{opt_str} requires a SERVICE'
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1370)             raise click.UsageError(msg)
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1371)     no_sv_options = [
1372)         params_by_str['--delete-globals'],
1373)         params_by_str['--clear'],
1374)         *options_in_group[StorageManagementOption],
1375)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1376)     for param in no_sv_options:
1377)         if is_param_set(param) and service:
1378)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1379)             msg = f'{opt_str} does not take a SERVICE argument'
1380)             raise click.UsageError(msg)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1381) 
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

1382)     if service == '':  # noqa: PLC1901
1383)         click.echo(
1384)             (
1385)                 f'{PROG_NAME}: Warning: An empty SERVICE is not '
1386)                 f'supported by vault(1).  For compatibility, this will be '
1387)                 f'treated as if SERVICE was not supplied, i.e., it will '
1388)                 f'error out, or operate on global settings.'
1389)             ),
1390)             err=True,
1391)         )
1392) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1393)     if edit_notes:
1394)         assert service is not None
1395)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1396)         text = DEFAULT_NOTES_TEMPLATE + configuration['services'].get(
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

1397)             service, cast(_types.VaultConfigServicesSettings, {})
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1398)         ).get('notes', '')
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1399)         notes_value = click.edit(text=text)
1400)         if notes_value is not None:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

1401)             notes_lines = collections.deque(notes_value.splitlines(True))  # noqa: FBT003
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1402)             while notes_lines:
1403)                 line = notes_lines.popleft()
1404)                 if line.startswith(DEFAULT_NOTES_MARKER):
1405)                     notes_value = ''.join(notes_lines)
1406)                     break
1407)             else:
1408)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

1409)                     err('Not saving new notes: user aborted request')
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1410)             configuration['services'].setdefault(service, {})['notes'] = (
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1411)                 notes_value.strip('\n')
1412)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1413)             put_config(configuration)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1414)     elif delete_service_settings:
1415)         assert service is not None
1416)         configuration = get_config()
1417)         if service in configuration['services']:
1418)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1419)             put_config(configuration)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1420)     elif delete_globals:
1421)         configuration = get_config()
1422)         if 'global' in configuration:
1423)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1424)             put_config(configuration)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1425)     elif clear_all_settings:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1426)         put_config({'services': {}})
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1427)     elif import_settings:
1428)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

1429)             # TODO(the-13th-letter): keep track of auto-close; try
1430)             # os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1431)             infile = (
1432)                 cast(TextIO, import_settings)
1433)                 if hasattr(import_settings, 'close')
1434)                 else click.open_file(os.fspath(import_settings), 'rt')
1435)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1436)             with infile:
1437)                 maybe_config = json.load(infile)
1438)         except json.JSONDecodeError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1439)             err(f'Cannot load config: cannot decode JSON: {e}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1440)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1441)             err(f'Cannot load config: {e.strerror}: {e.filename!r}')
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1442)         cleaned = _types.clean_up_falsy_vault_config_values(maybe_config)
1443)         if not _types.is_vault_config(maybe_config):
1444)             err(f'Cannot load config: {_INVALID_VAULT_CONFIG}')
1445)         assert cleaned is not None
1446)         for step in cleaned:
1447)             # These are never fatal errors, because the semantics of
1448)             # vault upon encountering these settings are ill-specified,
1449)             # but not ill-defined.
1450)             if step.action == 'replace':
1451)                 err_msg = (
1452)                     f'{PROG_NAME}: Warning: Replacing invalid value '
1453)                     f'{json.dumps(step.old_value)} for key '
1454)                     f'{_types.json_path(step.path)} with '
1455)                     f'{json.dumps(step.new_value)}.'
1456)                 )
1457)             else:
1458)                 err_msg = (
1459)                     f'{PROG_NAME}: Warning: Removing ineffective setting '
1460)                     f'{_types.json_path(step.path)} = '
1461)                     f'{json.dumps(step.old_value)}.'
1462)                 )
1463)             click.echo(err_msg, err=True)
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

1464)         if '' in maybe_config['services']:
1465)             err_msg = (
1466)                 f'{PROG_NAME}: Warning: An empty SERVICE is not '
1467)                 f'supported by vault(1), and the empty-string service '
1468)                 f'settings will be inaccessible and ineffective.  '
1469)                 f'To ensure that vault(1) and {PROG_NAME} see the settings, '
1470)                 f'move them into the "global" section.'
1471)             )
1472)             click.echo(err_msg, err=True)
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1473)         form = cast(
1474)             Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1475)             maybe_config.get('global', {}).get(
1476)                 'unicode_normalization_form', 'NFC'
1477)             ),
1478)         )
1479)         assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1480)         _check_for_misleading_passphrase(
1481)             ('global',),
1482)             cast(dict[str, Any], maybe_config.get('global', {})),
1483)             form=form,
1484)         )
1485)         for key, value in maybe_config['services'].items():
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1486)             _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1487)                 ('services', key),
1488)                 cast(dict[str, Any], value),
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1489)                 form=form,
1490)             )
Marco Ricci Align behavior with vault c...

Marco Ricci authored 2 months ago

1491)         configuration = get_config()
1492)         merged_config: collections.ChainMap[str, Any] = collections.ChainMap(
1493)             {
1494)                 'services': collections.ChainMap(
1495)                     maybe_config['services'],
1496)                     configuration['services'],
1497)                 ),
1498)             },
1499)             {'global': maybe_config['global']}
1500)             if 'global' in maybe_config
1501)             else {},
1502)             {'global': configuration['global']}
1503)             if 'global' in configuration
1504)             else {},
1505)         )
Marco Ricci Fix a typing issue

Marco Ricci authored 1 month ago

1506)         new_config: Any = {
Marco Ricci Align behavior with vault c...

Marco Ricci authored 2 months ago

1507)             k: dict(v) if isinstance(v, collections.ChainMap) else v
1508)             for k, v in sorted(merged_config.items())
1509)         }
1510)         assert _types.is_vault_config(new_config)
1511)         put_config(new_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1512)     elif export_settings:
1513)         configuration = get_config()
1514)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

1515)             # TODO(the-13th-letter): keep track of auto-close; try
1516)             # os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1517)             outfile = (
1518)                 cast(TextIO, export_settings)
1519)                 if hasattr(export_settings, 'close')
1520)                 else click.open_file(os.fspath(export_settings), 'wt')
1521)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1522)             with outfile:
1523)                 json.dump(configuration, outfile)
1524)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1525)             err(f'Cannot store config: {e.strerror}: {e.filename!r}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1526)     else:
1527)         configuration = get_config()
1528)         # This block could be type checked more stringently, but this
1529)         # would probably involve a lot of code repetition.  Since we
1530)         # have a type guarding function anyway, assert that we didn't
1531)         # make any mistakes at the end instead.
1532)         global_keys = {'key', 'phrase'}
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1533)         service_keys = {
1534)             'key',
1535)             'phrase',
1536)             'length',
1537)             'repeat',
1538)             'lower',
1539)             'upper',
1540)             'number',
1541)             'space',
1542)             'dash',
1543)             'symbol',
1544)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1545)         settings: collections.ChainMap[str, Any] = collections.ChainMap(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1546)             {
1547)                 k: v
1548)                 for k, v in locals().items()
1549)                 if k in service_keys and v is not None
1550)             },
1551)             cast(
1552)                 dict[str, Any],
1553)                 configuration['services'].get(service or '', {}),
1554)             ),
1555)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1556)         )
1557)         if use_key:
1558)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1559)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1560)                     'ASCII'
1561)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1562)             except IndexError:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

1563)                 err('No valid SSH key selected')
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1564)             except KeyError:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

1565)                 err('Cannot find running SSH agent; check SSH_AUTH_SOCK')
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

1566)             except NotImplementedError:
1567)                 err(
1568)                     'Cannot connect to SSH agent because '
1569)                     'this Python version does not support UNIX domain sockets'
1570)                 )
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1571)             except OSError as e:
1572)                 err(
1573)                     f'Cannot connect to SSH agent: {e.strerror}: '
1574)                     f'{e.filename!r}'
1575)                 )
Marco Ricci Add a specific error class...

Marco Ricci authored 4 months ago

1576)             except (
1577)                 LookupError,
1578)                 RuntimeError,
1579)                 ssh_agent.SSHAgentFailedError,
1580)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1581)                 err(str(e))
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1582)         elif use_phrase:
1583)             maybe_phrase = _prompt_for_passphrase()
1584)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

1585)                 err('No passphrase given')
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1586)             else:
1587)                 phrase = maybe_phrase
1588)         if store_config_only:
1589)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1590)             view = (
1591)                 collections.ChainMap(*settings.maps[:2])
1592)                 if service
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

1593)                 else collections.ChainMap(settings.maps[0], settings.maps[2])
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1594)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1595)             if use_key:
1596)                 view['key'] = key
1597)             elif use_phrase:
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

1598)                 view['phrase'] = phrase
1599)                 settings_type = 'service' if service else 'global'
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1600)                 _check_for_misleading_passphrase(
1601)                     ('services', service) if service else ('global',),
1602)                     {'phrase': phrase},
1603)                 )
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

1604)                 if 'key' in settings:
1605)                     err_msg = (
1606)                         f'{PROG_NAME}: Warning: Setting a {settings_type} '
1607)                         f'passphrase is ineffective because a key is also '
1608)                         f'set.'
1609)                     )
1610)                     click.echo(err_msg, err=True)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1611)             if not view.maps[0]:
1612)                 settings_type = 'service' if service else 'global'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1613)                 msg = (
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

1614)                     f'Cannot update {settings_type} settings without '
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1615)                     f'actual settings'
1616)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1617)                 raise click.UsageError(msg)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1618)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1619)                 configuration['services'].setdefault(service, {}).update(view)  # type: ignore[typeddict-item]
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1620)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1621)                 configuration.setdefault('global', {}).update(view)  # type: ignore[typeddict-item]
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

1622)             assert _types.is_vault_config(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1623)                 configuration
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

1624)             ), f'Invalid vault configuration: {configuration!r}'
Marco Ricci Fix error bubbling in outda...

Marco Ricci authored 4 months ago

1625)             put_config(configuration)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1626)         else:
1627)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1628)                 msg = 'SERVICE is required'
1629)                 raise click.UsageError(msg)
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1630)             kwargs: dict[str, Any] = {
1631)                 k: v
1632)                 for k, v in settings.items()
1633)                 if k in service_keys and v is not None
1634)             }
1635) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 5 months ago

1636)             def key_to_phrase(
1637)                 key: str | bytes | bytearray,
1638)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

1639)                 return vault.Vault.phrase_from_key(
Marco Ricci Shift misplaced local function

Marco Ricci authored 5 months ago

1640)                     base64.standard_b64decode(key)
1641)                 )
1642) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1643)             if use_phrase:
1644)                 form = cast(
1645)                     Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1646)                     configuration.get('global', {}).get(
1647)                         'unicode_normalization_form', 'NFC'
1648)                     ),
1649)                 )
1650)                 assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1651)                 _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1652)                     _ORIGIN.INTERACTIVE, {'phrase': phrase}, form=form
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1653)                 )
1654) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1655)             # If either --key or --phrase are given, use that setting.
1656)             # Otherwise, if both key and phrase are set in the config,
Marco Ricci Align behavior with vault c...

Marco Ricci authored 3 months ago

1657)             # use the key.  Otherwise, if only one of key and phrase is
1658)             # set in the config, use that one.  In all these above
1659)             # cases, set the phrase via vault.Vault.phrase_from_key if
1660)             # a key is given.  Finally, if nothing is set, error out.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1661)             if use_key or use_phrase:
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 5 months ago

1662)                 kwargs['phrase'] = key_to_phrase(key) if use_key else phrase
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1663)             elif kwargs.get('key'):
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 5 months ago

1664)                 kwargs['phrase'] = key_to_phrase(kwargs['key'])
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1665)             elif kwargs.get('phrase'):
1666)                 pass
1667)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1668)                 msg = (
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

1669)                     'No passphrase or key given on command-line '
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1670)                     'or in configuration'
1671)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1672)                 raise click.UsageError(msg)
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 5 months ago

1673)             kwargs.pop('key', '')
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

1674)             result = vault.Vault(**kwargs).generate(service)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1675)             click.echo(result.decode('ASCII'))