b5cb2824fdb57c10cc1021ebe284d33426824a28
Marco Ricci Change the author e-mail ad...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

352) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

448) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

483) 
484)     Args:
485)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

488) 
489)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

492) 
493)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

494)         KeyError:
495)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
496)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

511) 
512)     """
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

513)     with ssh_agent.SSHAgentClient.ensure_agent_subcontext(conn) as client:
Marco Ricci Fix test suite to actually...

Marco Ricci authored 3 weeks ago

514)         has_deterministic_signatures = client.has_deterministic_signatures()
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

521)         key, _comment = pair
Marco Ricci Fix test suite to actually...

Marco Ricci authored 3 weeks ago

522)         if (
523)             has_deterministic_signatures
524)             or vault.Vault._is_suitable_ssh_key(key)  # noqa: SLF001
525)         ):
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

529) 
530) 
531) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

534)     single_choice_prompt: str = 'Confirm this choice?',
535) ) -> int:
536)     """Prompt user for a choice among the given items.
537) 
538)     Print the heading, if any, then present the items to the user.  If
539)     there are multiple items, prompt the user for a selection, validate
540)     the choice, then return the list index of the selected item.  If
541)     there is only a single item, request confirmation for that item
542)     instead, and return the correct index.
543) 
544)     Args:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

547)         heading:
548)             A heading for the list of items, to print immediately
549)             before.  Defaults to a reasonable standard heading.  If
550)             explicitly empty, print no heading.
551)         single_choice_prompt:
552)             The confirmation prompt if there is only a single possible
553)             choice.  Defaults to a reasonable standard prompt.
554) 
555)     Returns:
556)         An index into the items sequence, indicating the user's
557)         selection.
558) 
559)     Raises:
560)         IndexError:
561)             The user made an invalid or empty selection, or requested an
562)             abort.
563) 
564)     """
565)     n = len(items)
566)     if heading:
567)         click.echo(click.style(heading, bold=True))
568)     for i, x in enumerate(items, start=1):
569)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
570)         click.echo(' ', nl=False)
571)         click.echo(x)
572)     if n > 1:
573)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
574)         choice = click.prompt(
575)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

576)             err=True,
577)             type=choices,
578)             show_choices=False,
579)             show_default=False,
580)             default='',
581)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

585)     prompt_suffix = (
586)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
587)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

588)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

589)         click.confirm(
590)             single_choice_prompt,
591)             prompt_suffix=prompt_suffix,
592)             err=True,
593)             abort=True,
594)             default=False,
595)             show_default=False,
596)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

597)     except click.Abort:
598)         raise IndexError(_EMPTY_SELECTION) from None
599)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

600) 
601) 
602) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

610) 
611)     Args:
612)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

615) 
616)     Returns:
617)         The selected SSH key.
618) 
619)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

620)         KeyError:
621)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
622)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

630)         IndexError:
631)             The user made an invalid or empty selection, or requested an
632)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

640)     """
641)     suitable_keys = list(_get_suitable_ssh_keys(conn))
642)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

647)         remaining_key_display_length = KEY_DISPLAY_LENGTH - 1 - len(keytype)
648)         key_extract = min(
649)             key_str,
650)             '...' + key_str[-remaining_key_display_length:],
651)             key=len,
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

656)         key_listing,
657)         heading='Suitable SSH keys:',
658)         single_choice_prompt='Use this key?',
659)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

660)     return suitable_keys[choice].key
661) 
662) 
663) def _prompt_for_passphrase() -> str:
664)     """Interactively prompt for the passphrase.
665) 
666)     Calls [`click.prompt`][] internally.  Moved into a separate function
667)     mainly for testing/mocking purposes.
668) 
669)     Returns:
670)         The user input.
671) 
672)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

673)     return cast(
674)         str,
675)         click.prompt(
676)             'Passphrase',
677)             default='',
678)             hide_input=True,
679)             show_default=False,
680)             err=True,
681)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

683) 
684) 
Marco Ricci Signal and list falsy value...

Marco Ricci authored 2 months ago

685) class _ORIGIN(enum.Enum):
686)     INTERACTIVE: str = 'interactive'
687) 
688) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

691)     value: dict[str, Any],
692)     *,
693)     form: Literal['NFC', 'NFD', 'NFKC', 'NFKD'] = 'NFC',
694) ) -> None:
695)     if 'phrase' in value:
696)         phrase = value['phrase']
697)         if not unicodedata.is_normalized(form, phrase):
Marco Ricci Signal and list falsy value...

Marco Ricci authored 2 months ago

698)             formatted_key = (
699)                 key.value
700)                 if isinstance(key, _ORIGIN)
701)                 else _types.json_path(key)
Marco Ricci Allow all textual strings,...

Marco Ricci authored 3 months ago

702)             )
703)             click.echo(
704)                 (
Marco Ricci Signal and list falsy value...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

708)                 ),
709)                 err=True,
710)             )
711) 
712) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

718) 
719)     Attributes:
720)         option_group_name:
721)             The name of the option group.  Used as a heading on the help
722)             text for options in this section.
723)         epilog:
724)             An epilog to print after listing the options in this
725)             section.
726) 
727)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

728) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

739) 
740) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

741)     """A [`click.Command`][] with support for help/option groups.
742) 
743)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
744)     further modified to support group epilogs.
745) 
746)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
747) 
748)     """
749) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

750)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

751)         self,
752)         ctx: click.Context,
753)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 2 months ago

762)         is an instance of some subclass of [`OptionGroupOption`][], then
763)         the section heading and the epilog are taken from the
764)         [`option_group_name`] [OptionGroupOption.option_group_name] and
765)         [`epilog`] [OptionGroupOption.epilog] attributes; otherwise, the
766)         section heading is "Options" (or "Other options" if there are
767)         other option groups) and the epilog is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

768) 
769)         Args:
770)             ctx:
771)                 The click context.
772)             formatter:
773)                 The formatter for the `--help` listing.
774) 
775)         """
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

776)         help_records: dict[str, list[tuple[str, str]]] = {}
777)         epilogs: dict[str, str] = {}
778)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

779)         if (  # pragma: no branch
780)             (help_opt := self.get_help_option(ctx)) is not None
781)             and help_opt not in params
782)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

783)             params.append(help_opt)
784)         for param in params:
785)             rec = param.get_help_record(ctx)
786)             if rec is not None:
787)                 if isinstance(param, OptionGroupOption):
788)                     group_name = param.option_group_name
789)                     epilogs.setdefault(group_name, param.epilog)
790)                 else:
791)                     group_name = ''
792)                 help_records.setdefault(group_name, []).append(rec)
793)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

794)         default_group_name = (
795)             'Other Options' if len(default_group) > 1 else 'Options'
796)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

797)         help_records[default_group_name] = default_group
798)         for group_name, records in help_records.items():
799)             with formatter.section(group_name):
800)                 formatter.write_dl(records)
801)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
802)             if epilog:
803)                 formatter.write_paragraph()
804)                 with formatter.indentation():
805)                     formatter.write_text(epilog)
806) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

818) 
819) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

827) 
828) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

835)     """
836) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

837) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

838) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

843)     """Check that the occurrence constraint is valid (int, 0 or larger).
844) 
845)     Args:
846)         ctx: The `click` context.
847)         param: The current command-line parameter.
848)         value: The parameter value to be checked.
849) 
850)     Returns:
851)         The parsed parameter value.
852) 
853)     Raises:
854)         click.BadParameter: The parameter value is invalid.
855) 
856)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

859)     if value is None:
860)         return value
861)     if isinstance(value, int):
862)         int_value = value
863)     else:
864)         try:
865)             int_value = int(value, 10)
866)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

872)     return int_value
873) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

874) 
875) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

909)     return int_value
910) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

911) 
912) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

913) # Enter notes below the line with the cut mark (ASCII scissors and
914) # dashes).  Lines above the cut mark (such as this one) will be ignored.
915) #
916) # If you wish to clear the notes, leave everything beyond the cut mark
917) # blank.  However, if you leave the *entire* file blank, also removing
918) # the cut mark, then the edit is aborted, and the old notes contents are
919) # retained.
920) #
921) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

923) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
924) 
925) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

940)     """,
941) )
942) @click.option(
943)     '-p',
944)     '--phrase',
945)     'use_phrase',
946)     is_flag=True,
947)     help='prompts you for your passphrase',
948)     cls=PasswordGenerationOption,
949) )
950) @click.option(
951)     '-k',
952)     '--key',
953)     'use_key',
954)     is_flag=True,
955)     help='uses your SSH private key to generate passwords',
956)     cls=PasswordGenerationOption,
957) )
958) @click.option(
959)     '-l',
960)     '--length',
961)     metavar='NUMBER',
962)     callback=_validate_length,
963)     help='emits password of length NUMBER',
964)     cls=PasswordGenerationOption,
965) )
966) @click.option(
967)     '-r',
968)     '--repeat',
969)     metavar='NUMBER',
970)     callback=_validate_occurrence_constraint,
971)     help='allows maximum of NUMBER repeated adjacent chars',
972)     cls=PasswordGenerationOption,
973) )
974) @click.option(
975)     '--lower',
976)     metavar='NUMBER',
977)     callback=_validate_occurrence_constraint,
978)     help='includes at least NUMBER lowercase letters',
979)     cls=PasswordGenerationOption,
980) )
981) @click.option(
982)     '--upper',
983)     metavar='NUMBER',
984)     callback=_validate_occurrence_constraint,
985)     help='includes at least NUMBER uppercase letters',
986)     cls=PasswordGenerationOption,
987) )
988) @click.option(
989)     '--number',
990)     metavar='NUMBER',
991)     callback=_validate_occurrence_constraint,
992)     help='includes at least NUMBER digits',
993)     cls=PasswordGenerationOption,
994) )
995) @click.option(
996)     '--space',
997)     metavar='NUMBER',
998)     callback=_validate_occurrence_constraint,
999)     help='includes at least NUMBER spaces',
1000)     cls=PasswordGenerationOption,
1001) )
1002) @click.option(
1003)     '--dash',
1004)     metavar='NUMBER',
1005)     callback=_validate_occurrence_constraint,
1006)     help='includes at least NUMBER "-" or "_"',
1007)     cls=PasswordGenerationOption,
1008) )
1009) @click.option(
1010)     '--symbol',
1011)     metavar='NUMBER',
1012)     callback=_validate_occurrence_constraint,
1013)     help='includes at least NUMBER symbol chars',
1014)     cls=PasswordGenerationOption,
1015) )
1016) @click.option(
1017)     '-n',
1018)     '--notes',
1019)     'edit_notes',
1020)     is_flag=True,
1021)     help='spawn an editor to edit notes for SERVICE',
1022)     cls=ConfigurationOption,
1023) )
1024) @click.option(
1025)     '-c',
1026)     '--config',
1027)     'store_config_only',
1028)     is_flag=True,
1029)     help='saves the given settings for SERVICE or global',
1030)     cls=ConfigurationOption,
1031) )
1032) @click.option(
1033)     '-x',
1034)     '--delete',
1035)     'delete_service_settings',
1036)     is_flag=True,
1037)     help='deletes settings for SERVICE',
1038)     cls=ConfigurationOption,
1039) )
1040) @click.option(
1041)     '--delete-globals',
1042)     is_flag=True,
1043)     help='deletes the global shared settings',
1044)     cls=ConfigurationOption,
1045) )
1046) @click.option(
1047)     '-X',
1048)     '--clear',
1049)     'clear_all_settings',
1050)     is_flag=True,
1051)     help='deletes all settings',
1052)     cls=ConfigurationOption,
1053) )
1054) @click.option(
1055)     '-e',
1056)     '--export',
1057)     'export_settings',
1058)     metavar='PATH',
1059)     help='export all saved settings into file PATH',
1060)     cls=StorageManagementOption,
1061) )
1062) @click.option(
1063)     '-i',
1064)     '--import',
1065)     'import_settings',
1066)     metavar='PATH',
1067)     help='import saved settings from file PATH',
1068)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1074)     ctx: click.Context,
1075)     /,
1076)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1077)     service: str | None = None,
1078)     use_phrase: bool = False,
1079)     use_key: bool = False,
1080)     length: int | None = None,
1081)     repeat: int | None = None,
1082)     lower: int | None = None,
1083)     upper: int | None = None,
1084)     number: int | None = None,
1085)     space: int | None = None,
1086)     dash: int | None = None,
1087)     symbol: int | None = None,
1088)     edit_notes: bool = False,
1089)     store_config_only: bool = False,
1090)     delete_service_settings: bool = False,
1091)     delete_globals: bool = False,
1092)     clear_all_settings: bool = False,
1093)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
1094)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1098)     Using a master passphrase or a master SSH key, derive a passphrase
1099)     for SERVICE, subject to length, character and character repetition
1100)     constraints.  The derivation is cryptographically strong, meaning
1101)     that even if a single passphrase is compromised, guessing the master
1102)     passphrase or a different service's passphrase is computationally
1103)     infeasible.  The derivation is also deterministic, given the same
1104)     inputs, thus the resulting passphrase need not be stored explicitly.
1105)     The service name and constraints themselves also need not be kept
1106)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1118) 
1119)     Parameters:
1120)         ctx (click.Context):
1121)             The `click` context.
1122) 
1123)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1176)         export_settings:
1177)             Command-line argument `-e`/`--export`.  If a file object,
1178)             then it must be open for writing and accept `str` inputs.
1179)             Otherwise, a filename to open for writing.  Using `-` for
1180)             standard output is supported.
1181)         import_settings:
1182)             Command-line argument `-i`/`--import`.  If a file object, it
1183)             must be open for reading and yield `str` values.  Otherwise,
1184)             a filename to open for reading.  Using `-` for standard
1185)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

1193)             # Use match/case here once Python 3.9 becomes unsupported.
1194)             if isinstance(param, PasswordGenerationOption):
1195)                 group = PasswordGenerationOption
1196)             elif isinstance(param, ConfigurationOption):
1197)                 group = ConfigurationOption
1198)             elif isinstance(param, StorageManagementOption):
1199)                 group = StorageManagementOption
1200)             elif isinstance(param, OptionGroupOption):
1201)                 raise AssertionError(  # noqa: DOC501,TRY003,TRY004
1202)                     f'Unknown option group for {param!r}'  # noqa: EM102
1203)                 )
1204)             else:
1205)                 group = click.Option
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1206)             options_in_group.setdefault(group, []).append(param)
1207)         params_by_str[param.human_readable_name] = param
1208)         for name in param.opts + param.secondary_opts:
1209)             params_by_str[name] = param
1210) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1214)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1218)         if isinstance(param, str):
1219)             param = params_by_str[param]
1220)         assert isinstance(param, click.Parameter)
1221)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1222)             return
1223)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1226)             assert isinstance(other, click.Parameter)
1227)             if other != param and is_param_set(other):
1228)                 opt_str = param.opts[0]
1229)                 other_str = other.opts[0]
1230)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1234)     def err(msg: str) -> NoReturn:
1235)         click.echo(f'{PROG_NAME}: {msg}', err=True)
1236)         ctx.exit(1)
1237) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1239)         try:
1240)             return _load_config()
1241)         except FileNotFoundError:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1242)             try:
1243)                 backup_config, exc = _migrate_and_load_old_config()
1244)             except FileNotFoundError:
1245)                 return {'services': {}}
1246)             old_name = os.path.basename(_config_filename())
1247)             new_name = os.path.basename(_config_filename(subsystem='vault'))
1248)             click.echo(
1249)                 (
1250)                     f'{PROG_NAME}: Using deprecated v0.1-style config file '
1251)                     f'{old_name!r}, instead of v0.2-style {new_name!r}.  '
1252)                     f'Support for v0.1-style config filenames will be '
1253)                     f'removed in v1.0.'
1254)                 ),
1255)                 err=True,
1256)             )
1257)             if isinstance(exc, OSError):
1258)                 click.echo(
1259)                     (
1260)                         f'{PROG_NAME}: Warning: Failed to migrate to '
1261)                         f'{new_name!r}: {exc.strerror}: {exc.filename!r}'
1262)                     ),
1263)                     err=True,
1264)                 )
1265)             else:
1266)                 click.echo(
1267)                     f'{PROG_NAME}: Successfully migrated to {new_name!r}.',
1268)                     err=True,
1269)                 )
1270)             return backup_config
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

1274)             err(f'Cannot load config: {e}')
1275) 
1276)     def put_config(config: _types.VaultConfig, /) -> None:
1277)         try:
1278)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1285) 
1286)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1291)                     opt, *options_in_group[PasswordGenerationOption]
1292)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1297)                 opt,
1298)                 *options_in_group[ConfigurationOption],
1299)                 *options_in_group[StorageManagementOption],
1300)             )
Marco Ricci Correctly model vault globa...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 2 months ago

1308)             raise click.UsageError(msg)  # noqa: DOC501
1309)     sv_options = [params_by_str['--notes'], params_by_str['--delete']]
1310)     for param in sv_options:
1311)         if is_param_set(param) and not service:
1312)             opt_str = param.opts[0]
1313)             msg = f'{opt_str} requires a SERVICE'
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

1315)     no_sv_options = [
1316)         params_by_str['--delete-globals'],
1317)         params_by_str['--clear'],
1318)         *options_in_group[StorageManagementOption],
1319)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

1326)     if service == '':  # noqa: PLC1901
1327)         click.echo(
1328)             (
1329)                 f'{PROG_NAME}: Warning: An empty SERVICE is not '
1330)                 f'supported by vault(1).  For compatibility, this will be '
1331)                 f'treated as if SERVICE was not supplied, i.e., it will '
1332)                 f'error out, or operate on global settings.'
1333)             ),
1334)             err=True,
1335)         )
1336) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1337)     if edit_notes:
1338)         assert service is not None
1339)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1346)             while notes_lines:
1347)                 line = notes_lines.popleft()
1348)                 if line.startswith(DEFAULT_NOTES_MARKER):
1349)                     notes_value = ''.join(notes_lines)
1350)                     break
1351)             else:
1352)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1355)                 notes_value.strip('\n')
1356)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1358)     elif delete_service_settings:
1359)         assert service is not None
1360)         configuration = get_config()
1361)         if service in configuration['services']:
1362)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1364)     elif delete_globals:
1365)         configuration = get_config()
1366)         if 'global' in configuration:
1367)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1371)     elif import_settings:
1372)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1375)             infile = (
1376)                 cast(TextIO, import_settings)
1377)                 if hasattr(import_settings, 'close')
1378)                 else click.open_file(os.fspath(import_settings), 'rt')
1379)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1380)             with infile:
1381)                 maybe_config = json.load(infile)
1382)         except json.JSONDecodeError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

1386)         cleaned = _types.clean_up_falsy_vault_config_values(maybe_config)
1387)         if not _types.is_vault_config(maybe_config):
1388)             err(f'Cannot load config: {_INVALID_VAULT_CONFIG}')
1389)         assert cleaned is not None
1390)         for step in cleaned:
1391)             # These are never fatal errors, because the semantics of
1392)             # vault upon encountering these settings are ill-specified,
1393)             # but not ill-defined.
1394)             if step.action == 'replace':
1395)                 err_msg = (
1396)                     f'{PROG_NAME}: Warning: Replacing invalid value '
1397)                     f'{json.dumps(step.old_value)} for key '
1398)                     f'{_types.json_path(step.path)} with '
1399)                     f'{json.dumps(step.new_value)}.'
1400)                 )
1401)             else:
1402)                 err_msg = (
1403)                     f'{PROG_NAME}: Warning: Removing ineffective setting '
1404)                     f'{_types.json_path(step.path)} = '
1405)                     f'{json.dumps(step.old_value)}.'
1406)                 )
1407)             click.echo(err_msg, err=True)
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

1408)         if '' in maybe_config['services']:
1409)             err_msg = (
1410)                 f'{PROG_NAME}: Warning: An empty SERVICE is not '
1411)                 f'supported by vault(1), and the empty-string service '
1412)                 f'settings will be inaccessible and ineffective.  '
1413)                 f'To ensure that vault(1) and {PROG_NAME} see the settings, '
1414)                 f'move them into the "global" section.'
1415)             )
1416)             click.echo(err_msg, err=True)
Marco Ricci Signal and list falsy value...

Marco Ricci authored 2 months ago

1417)         form = cast(
1418)             Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1419)             maybe_config.get('global', {}).get(
1420)                 'unicode_normalization_form', 'NFC'
1421)             ),
1422)         )
1423)         assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1424)         _check_for_misleading_passphrase(
1425)             ('global',),
1426)             cast(dict[str, Any], maybe_config.get('global', {})),
1427)             form=form,
1428)         )
1429)         for key, value in maybe_config['services'].items():
Marco Ricci Allow all textual strings,...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

1433)                 form=form,
1434)             )
Marco Ricci Align behavior with vault c...

Marco Ricci authored 2 months ago

1435)         configuration = get_config()
1436)         merged_config: collections.ChainMap[str, Any] = collections.ChainMap(
1437)             {
1438)                 'services': collections.ChainMap(
1439)                     maybe_config['services'],
1440)                     configuration['services'],
1441)                 ),
1442)             },
1443)             {'global': maybe_config['global']}
1444)             if 'global' in maybe_config
1445)             else {},
1446)             {'global': configuration['global']}
1447)             if 'global' in configuration
1448)             else {},
1449)         )
Marco Ricci Fix a typing issue

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1451)             k: dict(v) if isinstance(v, collections.ChainMap) else v
1452)             for k, v in sorted(merged_config.items())
1453)         }
1454)         assert _types.is_vault_config(new_config)
1455)         put_config(new_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1456)     elif export_settings:
1457)         configuration = get_config()
1458)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1461)             outfile = (
1462)                 cast(TextIO, export_settings)
1463)                 if hasattr(export_settings, 'close')
1464)                 else click.open_file(os.fspath(export_settings), 'wt')
1465)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1466)             with outfile:
1467)                 json.dump(configuration, outfile)
1468)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1477)         service_keys = {
1478)             'key',
1479)             'phrase',
1480)             'length',
1481)             'repeat',
1482)             'lower',
1483)             'upper',
1484)             'number',
1485)             'space',
1486)             'dash',
1487)             'symbol',
1488)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1490)             {
1491)                 k: v
1492)                 for k, v in locals().items()
1493)                 if k in service_keys and v is not None
1494)             },
1495)             cast(
1496)                 dict[str, Any],
1497)                 configuration['services'].get(service or '', {}),
1498)             ),
1499)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1500)         )
1501)         if use_key:
1502)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1503)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1504)                     'ASCII'
1505)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 2 months ago

1510)             except NotImplementedError:
1511)                 err(
1512)                     'Cannot connect to SSH agent because '
1513)                     'this Python version does not support UNIX domain sockets'
1514)                 )
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1515)             except OSError as e:
1516)                 err(
1517)                     f'Cannot connect to SSH agent: {e.strerror}: '
1518)                     f'{e.filename!r}'
1519)                 )
Marco Ricci Add a specific error class...

Marco Ricci authored 4 months ago

1520)             except (
1521)                 LookupError,
1522)                 RuntimeError,
1523)                 ssh_agent.SSHAgentFailedError,
1524)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1526)         elif use_phrase:
1527)             maybe_phrase = _prompt_for_passphrase()
1528)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1530)             else:
1531)                 phrase = maybe_phrase
1532)         if store_config_only:
1533)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1534)             view = (
1535)                 collections.ChainMap(*settings.maps[:2])
1536)                 if service
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1539)             if use_key:
1540)                 view['key'] = key
1541)             elif use_phrase:
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

1544)                 _check_for_misleading_passphrase(
1545)                     ('services', service) if service else ('global',),
1546)                     {'phrase': phrase},
1547)                 )
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

1548)                 if 'key' in settings:
1549)                     err_msg = (
1550)                         f'{PROG_NAME}: Warning: Setting a {settings_type} '
1551)                         f'passphrase is ineffective because a key is also '
1552)                         f'set.'
1553)                     )
1554)                     click.echo(err_msg, err=True)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1559)                     f'actual settings'
1560)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1562)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1564)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1570)         else:
1571)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

1574)             kwargs: dict[str, Any] = {
1575)                 k: v
1576)                 for k, v in settings.items()
1577)                 if k in service_keys and v is not None
1578)             }
1579) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 4 months ago

1580)             def key_to_phrase(
1581)                 key: str | bytes | bytearray,
1582)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

1584)                     base64.standard_b64decode(key)
1585)                 )
1586) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 3 months ago

1587)             if use_phrase:
1588)                 form = cast(
1589)                     Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1590)                     configuration.get('global', {}).get(
1591)                         'unicode_normalization_form', 'NFC'
1592)                     ),
1593)                 )
1594)                 assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1595)                 _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 months ago

1597)                 )
1598) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1609)             elif kwargs.get('phrase'):
1610)                 pass
1611)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1614)                     'or in configuration'
1615)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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