5c6045e10ca9c8b56432711dec5efb98b5892d55
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2) #
3) # SPDX-License-Identifier: MIT
4) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

12) import copy
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

13) import inspect
14) import json
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

15) import os
16) import socket
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

17) from typing import (
18)     TYPE_CHECKING,
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

20)     TextIO,
21)     cast,
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

23) 
24) import click
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

25) from typing_extensions import (
26)     Any,
27)     assert_never,
28) )
29) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

30) import derivepassphrase as dpp
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

32) 
33) if TYPE_CHECKING:
34)     import pathlib
35)     from collections.abc import (
36)         Iterator,
37)         Sequence,
38)     )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

39) 
40) __author__ = dpp.__author__
41) __version__ = dpp.__version__
42) 
43) __all__ = ('derivepassphrase',)
44) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

45) PROG_NAME = 'derivepassphrase'
46) KEY_DISPLAY_LENGTH = 30
47) 
48) # Error messages
49) _INVALID_VAULT_CONFIG = 'Invalid vault config'
50) _AGENT_COMMUNICATION_ERROR = 'Error communicating with the SSH agent'
51) _NO_USABLE_KEYS = 'No usable SSH keys were found'
52) _EMPTY_SELECTION = 'Empty selection'
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

53) 
54) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

55) def _config_filename() -> str | bytes | pathlib.Path:
56)     """Return the filename of the configuration file.
57) 
58)     The file is currently named `settings.json`, located within the
59)     configuration directory as determined by the `DERIVEPASSPHRASE_PATH`
60)     environment variable, or by [`click.get_app_dir`][] in POSIX
61)     mode.
62) 
63)     """
64)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

65)     path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir(
66)         PROG_NAME, force_posix=True
67)     )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

68)     return os.path.join(path, 'settings.json')
69) 
70) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

72)     """Load a vault(1)-compatible config from the application directory.
73) 
74)     The filename is obtained via
75)     [`derivepassphrase.cli._config_filename`][].  This must be an
76)     unencrypted JSON file.
77) 
78)     Returns:
79)         The vault settings.  See
80)         [`derivepassphrase.types.VaultConfig`][] for details.
81) 
82)     Raises:
83)         OSError:
84)             There was an OS error accessing the file.
85)         ValueError:
86)             The data loaded from the file is not a vault(1)-compatible
87)             config.
88) 
89)     """
90)     filename = _config_filename()
91)     with open(filename, 'rb') as fileobj:
92)         data = json.load(fileobj)
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

95)     return data
96) 
97) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

100) 
101)     The filename is obtained via
102)     [`derivepassphrase.cli._config_filename`][].  The config will be
103)     stored as an unencrypted JSON file.
104) 
105)     Args:
106)         config:
107)             vault configuration to save.
108) 
109)     Raises:
110)         OSError:
111)             There was an OS error accessing or writing the file.
112)         ValueError:
113)             The data cannot be stored as a vault(1)-compatible config.
114) 
115)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

118)     filename = _config_filename()
Marco Ricci Create the configuration di...

Marco Ricci authored 1 month ago

119)     filedir = os.path.dirname(os.path.abspath(filename))
120)     try:
121)         os.makedirs(filedir, exist_ok=False)
122)     except FileExistsError:
123)         if not os.path.isdir(filedir):
124)             raise
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

126)         json.dump(config, fileobj)
127) 
128) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

132)     """Yield all SSH keys suitable for passphrase derivation.
133) 
134)     Suitable SSH keys are queried from the running SSH agent (see
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

136) 
137)     Args:
138)         conn:
139)             An optional connection hint to the SSH agent; specifically,
140)             an SSH agent client, or a socket connected to an SSH agent.
141) 
142)             If an existing SSH agent client, then this client will be
143)             queried for the SSH keys, and otherwise left intact.
144) 
145)             If a socket, then a one-shot client will be constructed
146)             based on the socket to query the agent, and deconstructed
147)             afterwards.
148) 
149)             If neither are given, then the agent's socket location is
150)             looked up in the `SSH_AUTH_SOCK` environment variable, and
151)             used to construct/deconstruct a one-shot client, as in the
152)             previous case.
153) 
154)     Yields:
155)         :
156)             Every SSH key from the SSH agent that is suitable for
157)             passphrase derivation.
158) 
159)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

160)         KeyError:
161)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
162)             variable was not found.
163)         OSError:
164)             `conn` was a socket or `None`, and there was an error
165)             setting up a socket connection to the agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

169)         RuntimeError:
170)             There was an error communicating with the SSH agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

171) 
172)     """
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

174)     client_context: contextlib.AbstractContextManager[Any]
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

175)     match conn:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

176)         case ssh_agent.SSHAgentClient():
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

177)             client = conn
178)             client_context = contextlib.nullcontext()
179)         case socket.socket() | None:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

180)             client = ssh_agent.SSHAgentClient(socket=conn)
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

181)             client_context = client
182)         case _:  # pragma: no cover
183)             assert_never(conn)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

184)             msg = f'invalid connection hint: {conn!r}'
185)             raise TypeError(msg)
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

186)     with client_context:
187)         try:
188)             all_key_comment_pairs = list(client.list_keys())
189)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

198) 
199) 
200) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

203)     single_choice_prompt: str = 'Confirm this choice?',
204) ) -> int:
205)     """Prompt user for a choice among the given items.
206) 
207)     Print the heading, if any, then present the items to the user.  If
208)     there are multiple items, prompt the user for a selection, validate
209)     the choice, then return the list index of the selected item.  If
210)     there is only a single item, request confirmation for that item
211)     instead, and return the correct index.
212) 
213)     Args:
214)         heading:
215)             A heading for the list of items, to print immediately
216)             before.  Defaults to a reasonable standard heading.  If
217)             explicitly empty, print no heading.
218)         single_choice_prompt:
219)             The confirmation prompt if there is only a single possible
220)             choice.  Defaults to a reasonable standard prompt.
221) 
222)     Returns:
223)         An index into the items sequence, indicating the user's
224)         selection.
225) 
226)     Raises:
227)         IndexError:
228)             The user made an invalid or empty selection, or requested an
229)             abort.
230) 
231)     """
232)     n = len(items)
233)     if heading:
234)         click.echo(click.style(heading, bold=True))
235)     for i, x in enumerate(items, start=1):
236)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
237)         click.echo(' ', nl=False)
238)         click.echo(x)
239)     if n > 1:
240)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
241)         choice = click.prompt(
242)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

243)             err=True,
244)             type=choices,
245)             show_choices=False,
246)             show_default=False,
247)             default='',
248)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

252)     prompt_suffix = (
253)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
254)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

255)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

256)         click.confirm(
257)             single_choice_prompt,
258)             prompt_suffix=prompt_suffix,
259)             err=True,
260)             abort=True,
261)             default=False,
262)             show_default=False,
263)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

264)     except click.Abort:
265)         raise IndexError(_EMPTY_SELECTION) from None
266)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

267) 
268) 
269) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

271) ) -> bytes | bytearray:
272)     """Interactively select an SSH key for passphrase derivation.
273) 
274)     Suitable SSH keys are queried from the running SSH agent (see
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

275)     [`ssh_agent.SSHAgentClient.list_keys`][]), then the user is
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

276)     prompted interactively (see [`click.prompt`][]) for a selection.
277) 
278)     Args:
279)         conn:
280)             An optional connection hint to the SSH agent; specifically,
281)             an SSH agent client, or a socket connected to an SSH agent.
282) 
283)             If an existing SSH agent client, then this client will be
284)             queried for the SSH keys, and otherwise left intact.
285) 
286)             If a socket, then a one-shot client will be constructed
287)             based on the socket to query the agent, and deconstructed
288)             afterwards.
289) 
290)             If neither are given, then the agent's socket location is
291)             looked up in the `SSH_AUTH_SOCK` environment variable, and
292)             used to construct/deconstruct a one-shot client, as in the
293)             previous case.
294) 
295)     Returns:
296)         The selected SSH key.
297) 
298)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

299)         KeyError:
300)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
301)             variable was not found.
302)         OSError:
303)             `conn` was a socket or `None`, and there was an error
304)             setting up a socket connection to the agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

305)         IndexError:
306)             The user made an invalid or empty selection, or requested an
307)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

311)         RuntimeError:
312)             There was an error communicating with the SSH agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

313)     """
314)     suitable_keys = list(_get_suitable_ssh_keys(conn))
315)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

320)         key_prefix = (
321)             key_str
322)             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
323)             else key_str[:KEY_DISPLAY_LENGTH] + '...'
324)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

328)         key_listing,
329)         heading='Suitable SSH keys:',
330)         single_choice_prompt='Use this key?',
331)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

332)     return suitable_keys[choice].key
333) 
334) 
335) def _prompt_for_passphrase() -> str:
336)     """Interactively prompt for the passphrase.
337) 
338)     Calls [`click.prompt`][] internally.  Moved into a separate function
339)     mainly for testing/mocking purposes.
340) 
341)     Returns:
342)         The user input.
343) 
344)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

345)     return cast(
346)         str,
347)         click.prompt(
348)             'Passphrase',
349)             default='',
350)             hide_input=True,
351)             show_default=False,
352)             err=True,
353)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

355) 
356) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

358)     """A [`click.Option`][] with an associated group name and group epilog.
359) 
360)     Used by [`derivepassphrase.cli.CommandWithHelpGroups`][] to print
361)     help sections.  Each subclass contains its own group name and
362)     epilog.
363) 
364)     Attributes:
365)         option_group_name:
366)             The name of the option group.  Used as a heading on the help
367)             text for options in this section.
368)         epilog:
369)             An epilog to print after listing the options in this
370)             section.
371) 
372)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

373) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

374)     option_group_name: str = ''
375)     epilog: str = ''
376) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

377)     def __init__(self, *args: Any, **kwargs: Any) -> None:
378)         if self.__class__ == __class__:  # type: ignore[name-defined]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

382) 
383) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

384)     """A [`click.Command`][] with support for help/option groups.
385) 
386)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
387)     further modified to support group epilogs.
388) 
389)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
390) 
391)     """
392) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

393)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

394)         self,
395)         ctx: click.Context,
396)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

400)         This is a callback for [`click.Command.get_help`][] that
401)         implements the `--help` listing, by calling appropriate methods
402)         of the `formatter`.  We list all options (like the base
403)         implementation), but grouped into sections according to the
404)         concrete [`click.Option`][] subclass being used.  If the option
405)         is an instance of some subclass `X` of
406)         [`derivepassphrase.cli.OptionGroupOption`][], then the section
407)         heading and the epilog are taken from `X.option_group_name` and
408)         `X.epilog`; otherwise, the section heading is "Options" (or
409)         "Other options" if there are other option groups) and the epilog
410)         is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

411) 
412)         Args:
413)             ctx:
414)                 The click context.
415)             formatter:
416)                 The formatter for the `--help` listing.
417) 
Marco Ricci Add minor documentation rew...

Marco Ricci authored 2 months ago

418)         Returns:
419)             Nothing.  Output is generated by calling appropriate methods
420)             on `formatter` instead.
421) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

422)         """
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

423)         help_records: dict[str, list[tuple[str, str]]] = {}
424)         epilogs: dict[str, str] = {}
425)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

426)         if (  # pragma: no branch
427)             (help_opt := self.get_help_option(ctx)) is not None
428)             and help_opt not in params
429)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

430)             params.append(help_opt)
431)         for param in params:
432)             rec = param.get_help_record(ctx)
433)             if rec is not None:
434)                 if isinstance(param, OptionGroupOption):
435)                     group_name = param.option_group_name
436)                     epilogs.setdefault(group_name, param.epilog)
437)                 else:
438)                     group_name = ''
439)                 help_records.setdefault(group_name, []).append(rec)
440)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

441)         default_group_name = (
442)             'Other Options' if len(default_group) > 1 else 'Options'
443)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

444)         help_records[default_group_name] = default_group
445)         for group_name, records in help_records.items():
446)             with formatter.section(group_name):
447)                 formatter.write_dl(records)
448)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
449)             if epilog:
450)                 formatter.write_paragraph()
451)                 with formatter.indentation():
452)                     formatter.write_text(epilog)
453) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

465) 
466) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

474) 
475) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

482)     """
483) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

484) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

485) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

486)     ctx: click.Context,
487)     param: click.Parameter,
488)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

489) ) -> int | None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

490)     """Check that the occurrence constraint is valid (int, 0 or larger)."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

493)     if value is None:
494)         return value
495)     if isinstance(value, int):
496)         int_value = value
497)     else:
498)         try:
499)             int_value = int(value, 10)
500)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

506)     return int_value
507) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

508) 
509) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

510)     ctx: click.Context,
511)     param: click.Parameter,
512)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

513) ) -> int | None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

514)     """Check that the length is valid (int, 1 or larger)."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

517)     if value is None:
518)         return value
519)     if isinstance(value, int):
520)         int_value = value
521)     else:
522)         try:
523)             int_value = int(value, 10)
524)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

530)     return int_value
531) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

532) 
533) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

534) # Enter notes below the line with the cut mark (ASCII scissors and
535) # dashes).  Lines above the cut mark (such as this one) will be ignored.
536) #
537) # If you wish to clear the notes, leave everything beyond the cut mark
538) # blank.  However, if you leave the *entire* file blank, also removing
539) # the cut mark, then the edit is aborted, and the old notes contents are
540) # retained.
541) #
542) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

544) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
545) 
546) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

547) @click.command(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

556) 
557)         Configuration is stored in a directory according to the
558)         DERIVEPASSPHRASE_PATH variable, which defaults to
559)         `~/.derivepassphrase` on UNIX-like systems and
560)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
561)         The configuration is NOT encrypted, and you are STRONGLY
562)         discouraged from using a stored passphrase.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

563)     """,
564) )
565) @click.option(
566)     '-p',
567)     '--phrase',
568)     'use_phrase',
569)     is_flag=True,
570)     help='prompts you for your passphrase',
571)     cls=PasswordGenerationOption,
572) )
573) @click.option(
574)     '-k',
575)     '--key',
576)     'use_key',
577)     is_flag=True,
578)     help='uses your SSH private key to generate passwords',
579)     cls=PasswordGenerationOption,
580) )
581) @click.option(
582)     '-l',
583)     '--length',
584)     metavar='NUMBER',
585)     callback=_validate_length,
586)     help='emits password of length NUMBER',
587)     cls=PasswordGenerationOption,
588) )
589) @click.option(
590)     '-r',
591)     '--repeat',
592)     metavar='NUMBER',
593)     callback=_validate_occurrence_constraint,
594)     help='allows maximum of NUMBER repeated adjacent chars',
595)     cls=PasswordGenerationOption,
596) )
597) @click.option(
598)     '--lower',
599)     metavar='NUMBER',
600)     callback=_validate_occurrence_constraint,
601)     help='includes at least NUMBER lowercase letters',
602)     cls=PasswordGenerationOption,
603) )
604) @click.option(
605)     '--upper',
606)     metavar='NUMBER',
607)     callback=_validate_occurrence_constraint,
608)     help='includes at least NUMBER uppercase letters',
609)     cls=PasswordGenerationOption,
610) )
611) @click.option(
612)     '--number',
613)     metavar='NUMBER',
614)     callback=_validate_occurrence_constraint,
615)     help='includes at least NUMBER digits',
616)     cls=PasswordGenerationOption,
617) )
618) @click.option(
619)     '--space',
620)     metavar='NUMBER',
621)     callback=_validate_occurrence_constraint,
622)     help='includes at least NUMBER spaces',
623)     cls=PasswordGenerationOption,
624) )
625) @click.option(
626)     '--dash',
627)     metavar='NUMBER',
628)     callback=_validate_occurrence_constraint,
629)     help='includes at least NUMBER "-" or "_"',
630)     cls=PasswordGenerationOption,
631) )
632) @click.option(
633)     '--symbol',
634)     metavar='NUMBER',
635)     callback=_validate_occurrence_constraint,
636)     help='includes at least NUMBER symbol chars',
637)     cls=PasswordGenerationOption,
638) )
639) @click.option(
640)     '-n',
641)     '--notes',
642)     'edit_notes',
643)     is_flag=True,
644)     help='spawn an editor to edit notes for SERVICE',
645)     cls=ConfigurationOption,
646) )
647) @click.option(
648)     '-c',
649)     '--config',
650)     'store_config_only',
651)     is_flag=True,
652)     help='saves the given settings for SERVICE or global',
653)     cls=ConfigurationOption,
654) )
655) @click.option(
656)     '-x',
657)     '--delete',
658)     'delete_service_settings',
659)     is_flag=True,
660)     help='deletes settings for SERVICE',
661)     cls=ConfigurationOption,
662) )
663) @click.option(
664)     '--delete-globals',
665)     is_flag=True,
666)     help='deletes the global shared settings',
667)     cls=ConfigurationOption,
668) )
669) @click.option(
670)     '-X',
671)     '--clear',
672)     'clear_all_settings',
673)     is_flag=True,
674)     help='deletes all settings',
675)     cls=ConfigurationOption,
676) )
677) @click.option(
678)     '-e',
679)     '--export',
680)     'export_settings',
681)     metavar='PATH',
682)     type=click.Path(file_okay=True, allow_dash=True, exists=False),
683)     help='export all saved settings into file PATH',
684)     cls=StorageManagementOption,
685) )
686) @click.option(
687)     '-i',
688)     '--import',
689)     'import_settings',
690)     metavar='PATH',
691)     type=click.Path(file_okay=True, allow_dash=True, exists=False),
692)     help='import saved settings from file PATH',
693)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

696) @click.argument('service', required=False)
697) @click.pass_context
698) def derivepassphrase(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

699)     ctx: click.Context,
700)     /,
701)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

702)     service: str | None = None,
703)     use_phrase: bool = False,
704)     use_key: bool = False,
705)     length: int | None = None,
706)     repeat: int | None = None,
707)     lower: int | None = None,
708)     upper: int | None = None,
709)     number: int | None = None,
710)     space: int | None = None,
711)     dash: int | None = None,
712)     symbol: int | None = None,
713)     edit_notes: bool = False,
714)     store_config_only: bool = False,
715)     delete_service_settings: bool = False,
716)     delete_globals: bool = False,
717)     clear_all_settings: bool = False,
718)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
719)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

720) ) -> None:
721)     """Derive a strong passphrase, deterministically, from a master secret.
722) 
Marco Ricci Fill out README and documen...

Marco Ricci authored 2 months ago

723)     Using a master passphrase or a master SSH key, derive a passphrase
724)     for SERVICE, subject to length, character and character repetition
725)     constraints.  The derivation is cryptographically strong, meaning
726)     that even if a single passphrase is compromised, guessing the master
727)     passphrase or a different service's passphrase is computationally
728)     infeasible.  The derivation is also deterministic, given the same
729)     inputs, thus the resulting passphrase need not be stored explicitly.
730)     The service name and constraints themselves also need not be kept
731)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

741) 
742)     [CLICK]: https://click.palletsprojects.com/
743) 
744)     Parameters:
745)         ctx (click.Context):
746)             The `click` context.
747) 
748)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

775)         space:
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

776)             Command-line argument `--number`.  Same as `lower`, but for
777)             the space character.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

778)         dash:
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

779)             Command-line argument `--number`.  Same as `lower`, but for
780)             the hyphen-minus and underscore characters.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

781)         symbol:
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

782)             Command-line argument `--number`.  Same as `lower`, but for
783)             all other ASCII printable characters (except backquote).
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

801)         export_settings:
802)             Command-line argument `-e`/`--export`.  If a file object,
803)             then it must be open for writing and accept `str` inputs.
804)             Otherwise, a filename to open for writing.  Using `-` for
805)             standard output is supported.
806)         import_settings:
807)             Command-line argument `-i`/`--import`.  If a file object, it
808)             must be open for reading and yield `str` values.  Otherwise,
809)             a filename to open for reading.  Using `-` for standard
810)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

811) 
812)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

816)     for param in ctx.command.params:
817)         if isinstance(param, click.Option):
818)             group: type[click.Option]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

819)             match param:
820)                 case PasswordGenerationOption():
821)                     group = PasswordGenerationOption
822)                 case ConfigurationOption():
823)                     group = ConfigurationOption
824)                 case StorageManagementOption():
825)                     group = StorageManagementOption
826)                 case OptionGroupOption():
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

827)                     raise AssertionError(  # noqa: TRY003
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

828)                         f'Unknown option group for {param!r}'  # noqa: EM102
829)                     )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

830)                 case _:
831)                     group = click.Option
832)             options_in_group.setdefault(group, []).append(param)
833)         params_by_str[param.human_readable_name] = param
834)         for name in param.opts + param.secondary_opts:
835)             params_by_str[name] = param
836) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

840)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

844)         if isinstance(param, str):
845)             param = params_by_str[param]
846)         assert isinstance(param, click.Parameter)
847)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

848)             return
849)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

852)             assert isinstance(other, click.Parameter)
853)             if other != param and is_param_set(other):
854)                 opt_str = param.opts[0]
855)                 other_str = other.opts[0]
856)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

860)     def err(msg: str) -> NoReturn:
861)         click.echo(f'{PROG_NAME}: {msg}', err=True)
862)         ctx.exit(1)
863) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

865)         try:
866)             return _load_config()
867)         except FileNotFoundError:
868)             return {'services': {}}
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

872)             err(f'Cannot load config: {e}')
873) 
874)     def put_config(config: _types.VaultConfig, /) -> None:
875)         try:
876)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

883) 
884)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

889)                     opt, *options_in_group[PasswordGenerationOption]
890)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

895)                 opt,
896)                 *options_in_group[ConfigurationOption],
897)                 *options_in_group[StorageManagementOption],
898)             )
899)     sv_options = options_in_group[PasswordGenerationOption] + [
900)         params_by_str['--notes'],
901)         params_by_str['--delete'],
902)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

903)     sv_options.remove(params_by_str['--key'])
904)     sv_options.remove(params_by_str['--phrase'])
905)     for param in sv_options:
906)         if is_param_set(param) and not service:
907)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

908)             msg = f'{opt_str} requires a SERVICE'
909)             raise click.UsageError(msg)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

910)     for param in [params_by_str['--key'], params_by_str['--phrase']]:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

915)             msg = f'{opt_str} requires a SERVICE or --config'
916)             raise click.UsageError(msg)
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

917)     no_sv_options = [
918)         params_by_str['--delete-globals'],
919)         params_by_str['--clear'],
920)         *options_in_group[StorageManagementOption],
921)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

927) 
928)     if edit_notes:
929)         assert service is not None
930)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

934)         notes_value = click.edit(text=text)
935)         if notes_value is not None:
936)             notes_lines = collections.deque(notes_value.splitlines(True))
937)             while notes_lines:
938)                 line = notes_lines.popleft()
939)                 if line.startswith(DEFAULT_NOTES_MARKER):
940)                     notes_value = ''.join(notes_lines)
941)                     break
942)             else:
943)                 if not notes_value.strip():
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

946)                 notes_value.strip('\n')
947)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

949)     elif delete_service_settings:
950)         assert service is not None
951)         configuration = get_config()
952)         if service in configuration['services']:
953)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

955)     elif delete_globals:
956)         configuration = get_config()
957)         if 'global' in configuration:
958)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

962)     elif import_settings:
963)         try:
964)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

965)             infile = (
966)                 cast(TextIO, import_settings)
967)                 if hasattr(import_settings, 'close')
968)                 else click.open_file(os.fspath(import_settings), 'rt')
969)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

970)             with infile:
971)                 maybe_config = json.load(infile)
972)         except json.JSONDecodeError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

975)             err(f'Cannot load config: {e.strerror}: {e.filename!r}')
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

976)         if _types.is_vault_config(maybe_config):
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

977)             put_config(maybe_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

978)         else:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

979)             err(f'Cannot load config: {_INVALID_VAULT_CONFIG}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

980)     elif export_settings:
981)         configuration = get_config()
982)         try:
983)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

984)             outfile = (
985)                 cast(TextIO, export_settings)
986)                 if hasattr(export_settings, 'close')
987)                 else click.open_file(os.fspath(export_settings), 'wt')
988)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

989)             with outfile:
990)                 json.dump(configuration, outfile)
991)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1000)         service_keys = {
1001)             'key',
1002)             'phrase',
1003)             'length',
1004)             'repeat',
1005)             'lower',
1006)             'upper',
1007)             'number',
1008)             'space',
1009)             'dash',
1010)             'symbol',
1011)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1013)             {
1014)                 k: v
1015)                 for k, v in locals().items()
1016)                 if k in service_keys and v is not None
1017)             },
1018)             cast(
1019)                 dict[str, Any],
1020)                 configuration['services'].get(service or '', {}),
1021)             ),
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1022)             {},
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1023)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1024)         )
1025)         if use_key:
1026)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1027)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1028)                     'ASCII'
1029)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1030)             except IndexError:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1032)             except KeyError:
1033)                 err('cannot find running SSH agent; check SSH_AUTH_SOCK')
1034)             except OSError as e:
1035)                 err(
1036)                     f'Cannot connect to SSH agent: {e.strerror}: '
1037)                     f'{e.filename!r}'
1038)                 )
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

1039)             except (LookupError, RuntimeError) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1041)         elif use_phrase:
1042)             maybe_phrase = _prompt_for_passphrase()
1043)             if not maybe_phrase:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

1044)                 err('no passphrase given')
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1045)             else:
1046)                 phrase = maybe_phrase
1047)         if store_config_only:
1048)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1049)             view = (
1050)                 collections.ChainMap(*settings.maps[:2])
1051)                 if service
1052)                 else settings.parents.parents
1053)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1054)             if use_key:
1055)                 view['key'] = key
1056)                 for m in view.maps:
1057)                     m.pop('phrase', '')
1058)             elif use_phrase:
1059)                 view['phrase'] = phrase
1060)                 for m in view.maps:
1061)                     m.pop('key', '')
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1064)                 msg = (
1065)                     f'cannot update {settings_type} settings without '
1066)                     f'actual settings'
1067)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1069)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1071)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1074)                 configuration
1075)             ), f'invalid vault configuration: {configuration!r}'
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1076)             _save_config(configuration)
1077)         else:
1078)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1081)             kwargs: dict[str, Any] = {
1082)                 k: v
1083)                 for k, v in settings.items()
1084)                 if k in service_keys and v is not None
1085)             }
1086) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 1 month ago

1087)             def key_to_phrase(
1088)                 key: str | bytes | bytearray,
1089)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1091)                     base64.standard_b64decode(key)
1092)                 )
1093) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1094)             # If either --key or --phrase are given, use that setting.
1095)             # Otherwise, if both key and phrase are set in the config,
1096)             # one must be global (ignore it) and one must be
1097)             # service-specific (use that one). Otherwise, if only one of
1098)             # key and phrase is set in the config, use that one.  In all
1099)             # these above cases, set the phrase via
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

1100)             # derivepassphrase.vault.Vault.phrase_from_key if a key is
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1101)             # given. Finally, if nothing is set, error out.
1102)             if use_key or use_phrase:
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1104)             elif kwargs.get('phrase') and kwargs.get('key'):
1105)                 if any('key' in m for m in settings.maps[:2]):
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1109)             elif kwargs.get('phrase'):
1110)                 pass
1111)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1112)                 msg = (
1113)                     'no passphrase or key given on command-line '
1114)                     'or in configuration'
1115)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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