7d6ac080e84b06a116063b3cfec9c40620242b94
Marco Ricci Change the author e-mail ad...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

18) import os
19) import socket
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

21) from typing import (
22)     TYPE_CHECKING,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

25)     TextIO,
26)     cast,
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

28) 
29) import click
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

37) 
38) if TYPE_CHECKING:
39)     import pathlib
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

61) # Top-level
62) # =========
63) 
64) 
65) @click.command(
66)     context_settings={
67)         'help_option_names': ['-h', '--help'],
68)         'ignore_unknown_options': True,
69)         'allow_interspersed_args': False,
70)     },
71)     epilog=r"""
72)         Configuration is stored in a directory according to the
73)         DERIVEPASSPHRASE_PATH variable, which defaults to
74)         `~/.derivepassphrase` on UNIX-like systems and
75)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
Marco Ricci Fix minor typo, formatting...

Marco Ricci authored 2 months ago

76)     """,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

77) )
78) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
79) @click.argument('subcommand_args', nargs=-1, type=click.UNPROCESSED)
80) def derivepassphrase(
81)     *,
82)     subcommand_args: list[str],
83) ) -> None:
84)     """Derive a strong passphrase, deterministically, from a master secret.
85) 
86)     Using a master secret, derive a passphrase for a named service,
87)     subject to constraints e.g. on passphrase length, allowed
88)     characters, etc.  The exact derivation depends on the selected
89)     derivation scheme.  For each scheme, it is computationally
90)     infeasible to discern the master secret from the derived passphrase.
91)     The derivations are also deterministic, given the same inputs, thus
92)     the resulting passphrases need not be stored explicitly.  The
93)     service name and constraints themselves also generally need not be
94)     kept secret, depending on the scheme.
95) 
96)     The currently implemented subcommands are "vault" (for the scheme
97)     used by vault) and "export" (for exporting foreign configuration
98)     data).  See the respective `--help` output for instructions.  If no
99)     subcommand is given, we default to "vault".
100) 
101)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
102)     in v1.0, the subcommand must be specified explicitly.\f
103) 
104)     This is a [`click`][CLICK]-powered command-line interface function,
105)     and not intended for programmatic use.  Call with arguments
106)     `['--help']` to see full documentation of the interface.  (See also
107)     [`click.testing.CliRunner`][] for controlled, programmatic
108)     invocation.)
109) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

111) 
112)     """  # noqa: D301
113)     if subcommand_args and subcommand_args[0] == 'export':
114)         return derivepassphrase_export.main(
115)             args=subcommand_args[1:],
116)             prog_name=f'{PROG_NAME} export',
117)             standalone_mode=False,
118)         )
119)     if not (subcommand_args and subcommand_args[0] == 'vault'):
120)         click.echo(
121)             (
122)                 f'{PROG_NAME}: Deprecation warning: A subcommand will be '
123)                 f'required in v1.0. See --help for available subcommands.'
124)             ),
125)             err=True,
126)         )
127)         click.echo(
128)             f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
129)             err=True,
130)         )
131)     else:
132)         subcommand_args = subcommand_args[1:]
133)     return derivepassphrase_vault.main(
134)         args=subcommand_args,
135)         prog_name=f'{PROG_NAME} vault',
136)         standalone_mode=False,
137)     )
138) 
139) 
140) # Exporter
141) # ========
142) 
143) 
144) @click.command(
145)     context_settings={
146)         'help_option_names': ['-h', '--help'],
147)         'ignore_unknown_options': True,
148)         'allow_interspersed_args': False,
149)     }
150) )
151) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
152) @click.argument('subcommand_args', nargs=-1, type=click.UNPROCESSED)
153) def derivepassphrase_export(
154)     *,
155)     subcommand_args: list[str],
156) ) -> None:
157)     """Export a foreign configuration to standard output.
158) 
159)     Read a foreign system configuration, extract all information from
160)     it, and export the resulting configuration to standard output.
161) 
162)     The only available subcommand is "vault", which implements the
163)     vault-native configuration scheme.  If no subcommand is given, we
164)     default to "vault".
165) 
166)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
167)     in v1.0, the subcommand must be specified explicitly.\f
168) 
169)     This is a [`click`][CLICK]-powered command-line interface function,
170)     and not intended for programmatic use.  Call with arguments
171)     `['--help']` to see full documentation of the interface.  (See also
172)     [`click.testing.CliRunner`][] for controlled, programmatic
173)     invocation.)
174) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

176) 
177)     """  # noqa: D301
178)     if not (subcommand_args and subcommand_args[0] == 'vault'):
179)         click.echo(
180)             (
181)                 f'{PROG_NAME}: Deprecation warning: A subcommand will be '
182)                 f'required in v1.0. See --help for available subcommands.'
183)             ),
184)             err=True,
185)         )
186)         click.echo(
187)             f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
188)             err=True,
189)         )
190)     else:
191)         subcommand_args = subcommand_args[1:]
192)     return derivepassphrase_export_vault.main(
193)         args=subcommand_args,
194)         prog_name=f'{PROG_NAME} export vault',
195)         standalone_mode=False,
196)     )
197) 
198) 
199) def _load_data(
200)     fmt: Literal['v0.2', 'v0.3', 'storeroom'],
201)     path: str | bytes | os.PathLike[str],
202)     key: bytes,
203) ) -> Any:  # noqa: ANN401
204)     contents: bytes
205)     module: types.ModuleType
Marco Ricci Add support for Python 3.9

Marco Ricci authored 1 month ago

206)     # Use match/case here once Python 3.9 becomes unsupported.
207)     if fmt == 'v0.2':
208)         module = importlib.import_module(
209)             'derivepassphrase.exporter.vault_native'
210)         )
211)         if module.STUBBED:
212)             raise ModuleNotFoundError
213)         with open(path, 'rb') as infile:
214)             contents = base64.standard_b64decode(infile.read())
215)         return module.export_vault_native_data(
216)             contents, key, try_formats=['v0.2']
217)         )
218)     elif fmt == 'v0.3':  # noqa: RET505
219)         module = importlib.import_module(
220)             'derivepassphrase.exporter.vault_native'
221)         )
222)         if module.STUBBED:
223)             raise ModuleNotFoundError
224)         with open(path, 'rb') as infile:
225)             contents = base64.standard_b64decode(infile.read())
226)         return module.export_vault_native_data(
227)             contents, key, try_formats=['v0.3']
228)         )
229)     elif fmt == 'storeroom':
230)         module = importlib.import_module('derivepassphrase.exporter.storeroom')
231)         if module.STUBBED:
232)             raise ModuleNotFoundError
233)         return module.export_storeroom_data(path, key)
234)     else:  # pragma: no cover
235)         assert_never(fmt)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

236) 
237) 
238) @click.command(
239)     context_settings={'help_option_names': ['-h', '--help']},
240) )
241) @click.option(
242)     '-f',
243)     '--format',
244)     'formats',
245)     metavar='FMT',
246)     multiple=True,
247)     default=('v0.3', 'v0.2', 'storeroom'),
248)     type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
249)     help='try the following storage formats, in order (default: v0.3, v0.2)',
250) )
251) @click.option(
252)     '-k',
253)     '--key',
254)     metavar='K',
255)     help=(
256)         'use K as the storage master key '
257)         '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or '
258)         '`USERNAME` environment variables)'
259)     ),
260) )
261) @click.argument('path', metavar='PATH', required=True)
262) @click.pass_context
263) def derivepassphrase_export_vault(
264)     ctx: click.Context,
265)     /,
266)     *,
267)     path: str | bytes | os.PathLike[str],
268)     formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
269)     key: str | bytes | None = None,
270) ) -> None:
271)     """Export a vault-native configuration to standard output.
272) 
273)     Read the vault-native configuration at PATH, extract all information
274)     from it, and export the resulting configuration to standard output.
275)     Depending on the configuration format, PATH may either be a file or
276)     a directory.  Supports the vault "v0.2", "v0.3" and "storeroom"
277)     formats.
278) 
279)     If PATH is explicitly given as `VAULT_PATH`, then use the
280)     `VAULT_PATH` environment variable to determine the correct path.
281)     (Use `./VAULT_PATH` or similar to indicate a file/directory actually
282)     named `VAULT_PATH`.)
283) 
284)     """
285)     logging.basicConfig()
286)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
287)         path = exporter.get_vault_path()
288)     if key is None:
289)         key = exporter.get_vault_key()
290)     elif isinstance(key, str):  # pragma: no branch
291)         key = key.encode('utf-8')
292)     for fmt in formats:
293)         try:
294)             config = _load_data(fmt, path, key)
295)         except (
296)             IsADirectoryError,
297)             NotADirectoryError,
298)             ValueError,
299)             RuntimeError,
300)         ):
301)             logging.info('Cannot load as %s: %s', fmt, path)
302)             continue
303)         except OSError as exc:
304)             click.echo(
305)                 (
306)                     f'{PROG_NAME}: ERROR: Cannot parse {path!r} as '
307)                     f'a valid config: {exc.strerror}: {exc.filename!r}'
308)                 ),
309)                 err=True,
310)             )
311)             ctx.exit(1)
312)         except ModuleNotFoundError:
313)             # TODO(the-13th-letter): Use backslash continuation.
314)             # https://github.com/nedbat/coveragepy/issues/1836
315)             msg = f"""
316) {PROG_NAME}: ERROR: Cannot load the required Python module "cryptography".
317) {PROG_NAME}: INFO: pip users: see the "export" extra.
318) """.lstrip('\n')
319)             click.echo(msg, nl=False, err=True)
320)             ctx.exit(1)
321)         else:
322)             if not _types.is_vault_config(config):
323)                 click.echo(
324)                     f'{PROG_NAME}: ERROR: Invalid vault config: {config!r}',
325)                     err=True,
326)                 )
327)                 ctx.exit(1)
328)             click.echo(json.dumps(config, indent=2, sort_keys=True))
329)             break
330)     else:
331)         click.echo(
332)             f'{PROG_NAME}: ERROR: Cannot parse {path!r} as a valid config.',
333)             err=True,
334)         )
335)         ctx.exit(1)
336) 
337) 
338) # Vault
339) # =====
340) 
341) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 2 months ago

342) def _config_filename(
343)     subsystem: str | None = 'settings',
344) ) -> str | bytes | pathlib.Path:
345)     """Return the filename of the configuration file for the subsystem.
346) 
347)     The (implicit default) file is currently named `settings.json`,
348)     located within the configuration directory as determined by the
349)     `DERIVEPASSPHRASE_PATH` environment variable, or by
350)     [`click.get_app_dir`][] in POSIX mode.  Depending on the requested
351)     subsystem, this will usually be a different file within that
352)     directory.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

353) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 2 months ago

354)     Args:
355)         subsystem:
356)             Name of the configuration subsystem whose configuration
357)             filename to return.  If not given, return the old filename
358)             from before the subcommand migration.  If `None`, return the
359)             configuration directory instead.
360) 
361)     Raises:
362)         AssertionError:
363)             An unknown subsystem was passed.
364) 
365)     Deprecated:
366)         Since v0.2.0: The implicit default subsystem and the old
367)         configuration filename are deprecated, and will be removed in v1.0.
368)         The subsystem will be mandatory to specify.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

369) 
370)     """
371)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

375)     # Use match/case here once Python 3.9 becomes unsupported.
376)     if subsystem is None:
377)         return path
378)     elif subsystem in {'vault', 'settings'}:  # noqa: RET505
379)         filename = f'{subsystem}.json'
380)     else:  # pragma: no cover
381)         msg = f'Unknown configuration subsystem: {subsystem!r}'
382)         raise AssertionError(msg)
Marco Ricci Rename the configuration fi...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

384) 
385) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

391) 
392)     Returns:
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

394) 
395)     Raises:
396)         OSError:
397)             There was an OS error accessing the file.
398)         ValueError:
399)             The data loaded from the file is not a vault(1)-compatible
400)             config.
401) 
402)     """
Marco Ricci Rename the configuration fi...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

408)     return data
409) 
410) 
Marco Ricci Permit one flaky test and f...

Marco Ricci authored 1 month ago

411) def _migrate_and_load_old_config() -> tuple[
412)     _types.VaultConfig, OSError | None
413) ]:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

419) 
420)     Returns:
421)         The vault settings, and an optional exception encountered during
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

424) 
425)     Raises:
426)         OSError:
427)             There was an OS error accessing the old file.
428)         ValueError:
429)             The data loaded from the file is not a vault(1)-compatible
430)             config.
431) 
432)     """
433)     new_filename = _config_filename(subsystem='vault')
434)     old_filename = _config_filename()
435)     with open(old_filename, 'rb') as fileobj:
436)         data = json.load(fileobj)
437)     if not _types.is_vault_config(data):
438)         raise ValueError(_INVALID_VAULT_CONFIG)
439)     try:
440)         os.replace(old_filename, new_filename)
441)     except OSError as exc:
442)         return data, exc
443)     else:
444)         return data, None
445) 
446) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

449) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

452) 
453)     Args:
454)         config:
455)             vault configuration to save.
456) 
457)     Raises:
458)         OSError:
459)             There was an OS error accessing or writing the file.
460)         ValueError:
461)             The data cannot be stored as a vault(1)-compatible config.
462) 
463)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

467)     filedir = os.path.dirname(os.path.abspath(filename))
468)     try:
469)         os.makedirs(filedir, exist_ok=False)
470)     except FileExistsError:
471)         if not os.path.isdir(filedir):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

474)         json.dump(config, fileobj)
475) 
476) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

484) 
485)     Args:
486)         conn:
487)             An optional connection hint to the SSH agent; specifically,
488)             an SSH agent client, or a socket connected to an SSH agent.
489) 
490)             If an existing SSH agent client, then this client will be
491)             queried for the SSH keys, and otherwise left intact.
492) 
493)             If a socket, then a one-shot client will be constructed
494)             based on the socket to query the agent, and deconstructed
495)             afterwards.
496) 
497)             If neither are given, then the agent's socket location is
498)             looked up in the `SSH_AUTH_SOCK` environment variable, and
499)             used to construct/deconstruct a one-shot client, as in the
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 1 month ago

500)             previous case.  This requires the [`socket.AF_UNIX`][]
501)             symbol to exist.
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

502) 
503)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

506) 
507)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

508)         KeyError:
509)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
510)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

525) 
526)     """
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

527)     client: ssh_agent.SSHAgentClient
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 3 months ago

528)     client_context: contextlib.AbstractContextManager[Any]
Marco Ricci Add support for Python 3.9

Marco Ricci authored 1 month ago

529)     # Use match/case here once Python 3.9 becomes unsupported.
530)     if isinstance(conn, ssh_agent.SSHAgentClient):
531)         client = conn
532)         client_context = contextlib.nullcontext()
533)     elif isinstance(conn, socket.socket) or conn is None:
534)         client = ssh_agent.SSHAgentClient(socket=conn)
535)         client_context = client
536)     else:  # pragma: no cover
537)         assert_never(conn)
538)         msg = f'invalid connection hint: {conn!r}'
539)         raise TypeError(msg)  # noqa: DOC501
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

540)     with client_context:
541)         try:
542)             all_key_comment_pairs = list(client.list_keys())
543)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

544)             raise RuntimeError(_AGENT_COMMUNICATION_ERROR) from e
545)     suitable_keys = copy.copy(all_key_comment_pairs)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

546)     for pair in all_key_comment_pairs:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

547)         key, _comment = pair
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

548)         if vault.Vault._is_suitable_ssh_key(key):  # noqa: SLF001
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

549)             yield pair
550)     if not suitable_keys:  # pragma: no cover
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

552) 
553) 
554) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

557)     single_choice_prompt: str = 'Confirm this choice?',
558) ) -> int:
559)     """Prompt user for a choice among the given items.
560) 
561)     Print the heading, if any, then present the items to the user.  If
562)     there are multiple items, prompt the user for a selection, validate
563)     the choice, then return the list index of the selected item.  If
564)     there is only a single item, request confirmation for that item
565)     instead, and return the correct index.
566) 
567)     Args:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

570)         heading:
571)             A heading for the list of items, to print immediately
572)             before.  Defaults to a reasonable standard heading.  If
573)             explicitly empty, print no heading.
574)         single_choice_prompt:
575)             The confirmation prompt if there is only a single possible
576)             choice.  Defaults to a reasonable standard prompt.
577) 
578)     Returns:
579)         An index into the items sequence, indicating the user's
580)         selection.
581) 
582)     Raises:
583)         IndexError:
584)             The user made an invalid or empty selection, or requested an
585)             abort.
586) 
587)     """
588)     n = len(items)
589)     if heading:
590)         click.echo(click.style(heading, bold=True))
591)     for i, x in enumerate(items, start=1):
592)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
593)         click.echo(' ', nl=False)
594)         click.echo(x)
595)     if n > 1:
596)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
597)         choice = click.prompt(
598)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

599)             err=True,
600)             type=choices,
601)             show_choices=False,
602)             show_default=False,
603)             default='',
604)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

608)     prompt_suffix = (
609)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
610)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

611)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

612)         click.confirm(
613)             single_choice_prompt,
614)             prompt_suffix=prompt_suffix,
615)             err=True,
616)             abort=True,
617)             default=False,
618)             show_default=False,
619)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

620)     except click.Abort:
621)         raise IndexError(_EMPTY_SELECTION) from None
622)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

623) 
624) 
625) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

633) 
634)     Args:
635)         conn:
636)             An optional connection hint to the SSH agent; specifically,
637)             an SSH agent client, or a socket connected to an SSH agent.
638) 
639)             If an existing SSH agent client, then this client will be
640)             queried for the SSH keys, and otherwise left intact.
641) 
642)             If a socket, then a one-shot client will be constructed
643)             based on the socket to query the agent, and deconstructed
644)             afterwards.
645) 
646)             If neither are given, then the agent's socket location is
647)             looked up in the `SSH_AUTH_SOCK` environment variable, and
648)             used to construct/deconstruct a one-shot client, as in the
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 1 month ago

649)             previous case.  This requires the [`socket.AF_UNIX`][]
650)             symbol to exist.
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

651) 
652)     Returns:
653)         The selected SSH key.
654) 
655)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

656)         KeyError:
657)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
658)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

666)         IndexError:
667)             The user made an invalid or empty selection, or requested an
668)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

676)     """
677)     suitable_keys = list(_get_suitable_ssh_keys(conn))
678)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

680)     for key, comment in suitable_keys:
681)         keytype = unstring_prefix(key)[0].decode('ASCII')
682)         key_str = base64.standard_b64encode(key).decode('ASCII')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

683)         key_prefix = (
684)             key_str
685)             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
686)             else key_str[:KEY_DISPLAY_LENGTH] + '...'
687)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

688)         comment_str = comment.decode('UTF-8', errors='replace')
689)         key_listing.append(f'{keytype} {key_prefix} {comment_str}')
690)     choice = _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

691)         key_listing,
692)         heading='Suitable SSH keys:',
693)         single_choice_prompt='Use this key?',
694)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

695)     return suitable_keys[choice].key
696) 
697) 
698) def _prompt_for_passphrase() -> str:
699)     """Interactively prompt for the passphrase.
700) 
701)     Calls [`click.prompt`][] internally.  Moved into a separate function
702)     mainly for testing/mocking purposes.
703) 
704)     Returns:
705)         The user input.
706) 
707)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 3 months ago

708)     return cast(
709)         str,
710)         click.prompt(
711)             'Passphrase',
712)             default='',
713)             hide_input=True,
714)             show_default=False,
715)             err=True,
716)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

718) 
719) 
Marco Ricci Signal and list falsy value...

Marco Ricci authored 1 month ago

720) class _ORIGIN(enum.Enum):
721)     INTERACTIVE: str = 'interactive'
722) 
723) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

726)     value: dict[str, Any],
727)     *,
728)     form: Literal['NFC', 'NFD', 'NFKC', 'NFKD'] = 'NFC',
729) ) -> None:
730)     if 'phrase' in value:
731)         phrase = value['phrase']
732)         if not unicodedata.is_normalized(form, phrase):
Marco Ricci Signal and list falsy value...

Marco Ricci authored 1 month ago

733)             formatted_key = (
734)                 key.value
735)                 if isinstance(key, _ORIGIN)
736)                 else _types.json_path(key)
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

737)             )
738)             click.echo(
739)                 (
Marco Ricci Signal and list falsy value...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

743)                 ),
744)                 err=True,
745)             )
746) 
747) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

753) 
754)     Attributes:
755)         option_group_name:
756)             The name of the option group.  Used as a heading on the help
757)             text for options in this section.
758)         epilog:
759)             An epilog to print after listing the options in this
760)             section.
761) 
762)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

763) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

774) 
775) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

776)     """A [`click.Command`][] with support for help/option groups.
777) 
778)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
779)     further modified to support group epilogs.
780) 
781)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
782) 
783)     """
784) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

785)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

786)         self,
787)         ctx: click.Context,
788)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

797)         is an instance of some subclass of [`OptionGroupOption`][], then
798)         the section heading and the epilog are taken from the
799)         [`option_group_name`] [OptionGroupOption.option_group_name] and
800)         [`epilog`] [OptionGroupOption.epilog] attributes; otherwise, the
801)         section heading is "Options" (or "Other options" if there are
802)         other option groups) and the epilog is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

803) 
804)         Args:
805)             ctx:
806)                 The click context.
807)             formatter:
808)                 The formatter for the `--help` listing.
809) 
810)         """
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

811)         help_records: dict[str, list[tuple[str, str]]] = {}
812)         epilogs: dict[str, str] = {}
813)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

814)         if (  # pragma: no branch
815)             (help_opt := self.get_help_option(ctx)) is not None
816)             and help_opt not in params
817)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

818)             params.append(help_opt)
819)         for param in params:
820)             rec = param.get_help_record(ctx)
821)             if rec is not None:
822)                 if isinstance(param, OptionGroupOption):
823)                     group_name = param.option_group_name
824)                     epilogs.setdefault(group_name, param.epilog)
825)                 else:
826)                     group_name = ''
827)                 help_records.setdefault(group_name, []).append(rec)
828)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

829)         default_group_name = (
830)             'Other Options' if len(default_group) > 1 else 'Options'
831)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

832)         help_records[default_group_name] = default_group
833)         for group_name, records in help_records.items():
834)             with formatter.section(group_name):
835)                 formatter.write_dl(records)
836)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
837)             if epilog:
838)                 formatter.write_paragraph()
839)                 with formatter.indentation():
840)                     formatter.write_text(epilog)
841) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

853) 
854) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

862) 
863) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

870)     """
871) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

872) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

873) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

878)     """Check that the occurrence constraint is valid (int, 0 or larger).
879) 
880)     Args:
881)         ctx: The `click` context.
882)         param: The current command-line parameter.
883)         value: The parameter value to be checked.
884) 
885)     Returns:
886)         The parsed parameter value.
887) 
888)     Raises:
889)         click.BadParameter: The parameter value is invalid.
890) 
891)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

894)     if value is None:
895)         return value
896)     if isinstance(value, int):
897)         int_value = value
898)     else:
899)         try:
900)             int_value = int(value, 10)
901)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

907)     return int_value
908) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

909) 
910) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

915)     """Check that the length is valid (int, 1 or larger).
916) 
917)     Args:
918)         ctx: The `click` context.
919)         param: The current command-line parameter.
920)         value: The parameter value to be checked.
921) 
922)     Returns:
923)         The parsed parameter value.
924) 
925)     Raises:
926)         click.BadParameter: The parameter value is invalid.
927) 
928)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

931)     if value is None:
932)         return value
933)     if isinstance(value, int):
934)         int_value = value
935)     else:
936)         try:
937)             int_value = int(value, 10)
938)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

944)     return int_value
945) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

946) 
947) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

948) # Enter notes below the line with the cut mark (ASCII scissors and
949) # dashes).  Lines above the cut mark (such as this one) will be ignored.
950) #
951) # If you wish to clear the notes, leave everything beyond the cut mark
952) # blank.  However, if you leave the *entire* file blank, also removing
953) # the cut mark, then the edit is aborted, and the old notes contents are
954) # retained.
955) #
956) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

958) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
959) 
960) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

961) @click.command(
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

962)     # 'vault',
963)     # help="derivation scheme compatible with James Coglan's vault(1)",
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

1109)     ctx: click.Context,
1110)     /,
1111)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1112)     service: str | None = None,
1113)     use_phrase: bool = False,
1114)     use_key: bool = False,
1115)     length: int | None = None,
1116)     repeat: int | None = None,
1117)     lower: int | None = None,
1118)     upper: int | None = None,
1119)     number: int | None = None,
1120)     space: int | None = None,
1121)     dash: int | None = None,
1122)     symbol: int | None = None,
1123)     edit_notes: bool = False,
1124)     store_config_only: bool = False,
1125)     delete_service_settings: bool = False,
1126)     delete_globals: bool = False,
1127)     clear_all_settings: bool = False,
1128)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
1129)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

1133)     Using a master passphrase or a master SSH key, derive a passphrase
1134)     for SERVICE, subject to length, character and character repetition
1135)     constraints.  The derivation is cryptographically strong, meaning
1136)     that even if a single passphrase is compromised, guessing the master
1137)     passphrase or a different service's passphrase is computationally
1138)     infeasible.  The derivation is also deterministic, given the same
1139)     inputs, thus the resulting passphrase need not be stored explicitly.
1140)     The service name and constraints themselves also need not be kept
1141)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

1153) 
1154)     Parameters:
1155)         ctx (click.Context):
1156)             The `click` context.
1157) 
1158)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

1211)         export_settings:
1212)             Command-line argument `-e`/`--export`.  If a file object,
1213)             then it must be open for writing and accept `str` inputs.
1214)             Otherwise, a filename to open for writing.  Using `-` for
1215)             standard output is supported.
1216)         import_settings:
1217)             Command-line argument `-i`/`--import`.  If a file object, it
1218)             must be open for reading and yield `str` values.  Otherwise,
1219)             a filename to open for reading.  Using `-` for standard
1220)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

1228)             # Use match/case here once Python 3.9 becomes unsupported.
1229)             if isinstance(param, PasswordGenerationOption):
1230)                 group = PasswordGenerationOption
1231)             elif isinstance(param, ConfigurationOption):
1232)                 group = ConfigurationOption
1233)             elif isinstance(param, StorageManagementOption):
1234)                 group = StorageManagementOption
1235)             elif isinstance(param, OptionGroupOption):
1236)                 raise AssertionError(  # noqa: DOC501,TRY003,TRY004
1237)                     f'Unknown option group for {param!r}'  # noqa: EM102
1238)                 )
1239)             else:
1240)                 group = click.Option
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1241)             options_in_group.setdefault(group, []).append(param)
1242)         params_by_str[param.human_readable_name] = param
1243)         for name in param.opts + param.secondary_opts:
1244)             params_by_str[name] = param
1245) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

1249)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

1253)         if isinstance(param, str):
1254)             param = params_by_str[param]
1255)         assert isinstance(param, click.Parameter)
1256)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1257)             return
1258)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1261)             assert isinstance(other, click.Parameter)
1262)             if other != param and is_param_set(other):
1263)                 opt_str = param.opts[0]
1264)                 other_str = other.opts[0]
1265)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

1269)     def err(msg: str) -> NoReturn:
1270)         click.echo(f'{PROG_NAME}: {msg}', err=True)
1271)         ctx.exit(1)
1272) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1274)         try:
1275)             return _load_config()
1276)         except FileNotFoundError:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 2 months ago

1277)             try:
1278)                 backup_config, exc = _migrate_and_load_old_config()
1279)             except FileNotFoundError:
1280)                 return {'services': {}}
1281)             old_name = os.path.basename(_config_filename())
1282)             new_name = os.path.basename(_config_filename(subsystem='vault'))
1283)             click.echo(
1284)                 (
1285)                     f'{PROG_NAME}: Using deprecated v0.1-style config file '
1286)                     f'{old_name!r}, instead of v0.2-style {new_name!r}.  '
1287)                     f'Support for v0.1-style config filenames will be '
1288)                     f'removed in v1.0.'
1289)                 ),
1290)                 err=True,
1291)             )
1292)             if isinstance(exc, OSError):
1293)                 click.echo(
1294)                     (
1295)                         f'{PROG_NAME}: Warning: Failed to migrate to '
1296)                         f'{new_name!r}: {exc.strerror}: {exc.filename!r}'
1297)                     ),
1298)                     err=True,
1299)                 )
1300)             else:
1301)                 click.echo(
1302)                     f'{PROG_NAME}: Successfully migrated to {new_name!r}.',
1303)                     err=True,
1304)                 )
1305)             return backup_config
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

1309)             err(f'Cannot load config: {e}')
1310) 
1311)     def put_config(config: _types.VaultConfig, /) -> None:
1312)         try:
1313)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1320) 
1321)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

1326)                     opt, *options_in_group[PasswordGenerationOption]
1327)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

1332)                 opt,
1333)                 *options_in_group[ConfigurationOption],
1334)                 *options_in_group[StorageManagementOption],
1335)             )
Marco Ricci Correctly model vault globa...

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 weeks ago

1343)             raise click.UsageError(msg)  # noqa: DOC501
1344)     sv_options = [params_by_str['--notes'], params_by_str['--delete']]
1345)     for param in sv_options:
1346)         if is_param_set(param) and not service:
1347)             opt_str = param.opts[0]
1348)             msg = f'{opt_str} requires a SERVICE'
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

1350)     no_sv_options = [
1351)         params_by_str['--delete-globals'],
1352)         params_by_str['--clear'],
1353)         *options_in_group[StorageManagementOption],
1354)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 weeks ago

1361)     if service == '':  # noqa: PLC1901
1362)         click.echo(
1363)             (
1364)                 f'{PROG_NAME}: Warning: An empty SERVICE is not '
1365)                 f'supported by vault(1).  For compatibility, this will be '
1366)                 f'treated as if SERVICE was not supplied, i.e., it will '
1367)                 f'error out, or operate on global settings.'
1368)             ),
1369)             err=True,
1370)         )
1371) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1372)     if edit_notes:
1373)         assert service is not None
1374)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

1381)             while notes_lines:
1382)                 line = notes_lines.popleft()
1383)                 if line.startswith(DEFAULT_NOTES_MARKER):
1384)                     notes_value = ''.join(notes_lines)
1385)                     break
1386)             else:
1387)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

1390)                 notes_value.strip('\n')
1391)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

1393)     elif delete_service_settings:
1394)         assert service is not None
1395)         configuration = get_config()
1396)         if service in configuration['services']:
1397)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

1399)     elif delete_globals:
1400)         configuration = get_config()
1401)         if 'global' in configuration:
1402)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

1406)     elif import_settings:
1407)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

1410)             infile = (
1411)                 cast(TextIO, import_settings)
1412)                 if hasattr(import_settings, 'close')
1413)                 else click.open_file(os.fspath(import_settings), 'rt')
1414)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1415)             with infile:
1416)                 maybe_config = json.load(infile)
1417)         except json.JSONDecodeError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1421)         cleaned = _types.clean_up_falsy_vault_config_values(maybe_config)
1422)         if not _types.is_vault_config(maybe_config):
1423)             err(f'Cannot load config: {_INVALID_VAULT_CONFIG}')
1424)         assert cleaned is not None
1425)         for step in cleaned:
1426)             # These are never fatal errors, because the semantics of
1427)             # vault upon encountering these settings are ill-specified,
1428)             # but not ill-defined.
1429)             if step.action == 'replace':
1430)                 err_msg = (
1431)                     f'{PROG_NAME}: Warning: Replacing invalid value '
1432)                     f'{json.dumps(step.old_value)} for key '
1433)                     f'{_types.json_path(step.path)} with '
1434)                     f'{json.dumps(step.new_value)}.'
1435)                 )
1436)             else:
1437)                 err_msg = (
1438)                     f'{PROG_NAME}: Warning: Removing ineffective setting '
1439)                     f'{_types.json_path(step.path)} = '
1440)                     f'{json.dumps(step.old_value)}.'
1441)                 )
1442)             click.echo(err_msg, err=True)
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 4 weeks ago

1443)         if '' in maybe_config['services']:
1444)             err_msg = (
1445)                 f'{PROG_NAME}: Warning: An empty SERVICE is not '
1446)                 f'supported by vault(1), and the empty-string service '
1447)                 f'settings will be inaccessible and ineffective.  '
1448)                 f'To ensure that vault(1) and {PROG_NAME} see the settings, '
1449)                 f'move them into the "global" section.'
1450)             )
1451)             click.echo(err_msg, err=True)
Marco Ricci Signal and list falsy value...

Marco Ricci authored 1 month ago

1452)         form = cast(
1453)             Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1454)             maybe_config.get('global', {}).get(
1455)                 'unicode_normalization_form', 'NFC'
1456)             ),
1457)         )
1458)         assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1459)         _check_for_misleading_passphrase(
1460)             ('global',),
1461)             cast(dict[str, Any], maybe_config.get('global', {})),
1462)             form=form,
1463)         )
1464)         for key, value in maybe_config['services'].items():
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1468)                 form=form,
1469)             )
Marco Ricci Align behavior with vault c...

Marco Ricci authored 4 weeks ago

1470)         configuration = get_config()
1471)         merged_config: collections.ChainMap[str, Any] = collections.ChainMap(
1472)             {
1473)                 'services': collections.ChainMap(
1474)                     maybe_config['services'],
1475)                     configuration['services'],
1476)                 ),
1477)             },
1478)             {'global': maybe_config['global']}
1479)             if 'global' in maybe_config
1480)             else {},
1481)             {'global': configuration['global']}
1482)             if 'global' in configuration
1483)             else {},
1484)         )
1485)         new_config = {
1486)             k: dict(v) if isinstance(v, collections.ChainMap) else v
1487)             for k, v in sorted(merged_config.items())
1488)         }
1489)         assert _types.is_vault_config(new_config)
1490)         put_config(new_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1491)     elif export_settings:
1492)         configuration = get_config()
1493)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

1496)             outfile = (
1497)                 cast(TextIO, export_settings)
1498)                 if hasattr(export_settings, 'close')
1499)                 else click.open_file(os.fspath(export_settings), 'wt')
1500)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1501)             with outfile:
1502)                 json.dump(configuration, outfile)
1503)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

1512)         service_keys = {
1513)             'key',
1514)             'phrase',
1515)             'length',
1516)             'repeat',
1517)             'lower',
1518)             'upper',
1519)             'number',
1520)             'space',
1521)             'dash',
1522)             'symbol',
1523)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

1525)             {
1526)                 k: v
1527)                 for k, v in locals().items()
1528)                 if k in service_keys and v is not None
1529)             },
1530)             cast(
1531)                 dict[str, Any],
1532)                 configuration['services'].get(service or '', {}),
1533)             ),
1534)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1535)         )
1536)         if use_key:
1537)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1538)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1539)                     'ASCII'
1540)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1545)             except NotImplementedError:
1546)                 err(
1547)                     'Cannot connect to SSH agent because '
1548)                     'this Python version does not support UNIX domain sockets'
1549)                 )
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

1550)             except OSError as e:
1551)                 err(
1552)                     f'Cannot connect to SSH agent: {e.strerror}: '
1553)                     f'{e.filename!r}'
1554)                 )
Marco Ricci Add a specific error class...

Marco Ricci authored 2 months ago

1555)             except (
1556)                 LookupError,
1557)                 RuntimeError,
1558)                 ssh_agent.SSHAgentFailedError,
1559)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

1561)         elif use_phrase:
1562)             maybe_phrase = _prompt_for_passphrase()
1563)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

1565)             else:
1566)                 phrase = maybe_phrase
1567)         if store_config_only:
1568)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1569)             view = (
1570)                 collections.ChainMap(*settings.maps[:2])
1571)                 if service
Marco Ricci Fix missing consideration o...

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1574)             if use_key:
1575)                 view['key'] = key
1576)             elif use_phrase:
Marco Ricci Fix missing consideration o...

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 2 months ago

1579)                 _check_for_misleading_passphrase(
1580)                     ('services', service) if service else ('global',),
1581)                     {'phrase': phrase},
1582)                 )
Marco Ricci Fix missing consideration o...

Marco Ricci authored 4 weeks ago

1583)                 if 'key' in settings:
1584)                     err_msg = (
1585)                         f'{PROG_NAME}: Warning: Setting a {settings_type} '
1586)                         f'passphrase is ineffective because a key is also '
1587)                         f'set.'
1588)                     )
1589)                     click.echo(err_msg, err=True)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

1594)                     f'actual settings'
1595)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1597)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1599)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

1605)         else:
1606)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

1609)             kwargs: dict[str, Any] = {
1610)                 k: v
1611)                 for k, v in settings.items()
1612)                 if k in service_keys and v is not None
1613)             }
1614) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 3 months ago

1615)             def key_to_phrase(
1616)                 key: str | bytes | bytearray,
1617)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

1619)                     base64.standard_b64decode(key)
1620)                 )
1621) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

1622)             if use_phrase:
1623)                 form = cast(
1624)                     Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1625)                     configuration.get('global', {}).get(
1626)                         'unicode_normalization_form', 'NFC'
1627)                     ),
1628)                 )
1629)                 assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1630)                 _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1632)                 )
1633) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1644)             elif kwargs.get('phrase'):
1645)                 pass
1646)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

1649)                     'or in configuration'
1650)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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