0ea17f021eb19d00de8d7583cab2cf260ce6ff1f
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,
19)     TextIO,
20)     cast,
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

29) import derivepassphrase as dpp
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

30) import ssh_agent_client
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

31) import ssh_agent_client.types
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

96)     return data
97) 
98) 
99) def _save_config(config: dpp_types.VaultConfig, /) -> None:
Marco Ricci Create the configuration di...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

130) def _get_suitable_ssh_keys(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

132) ) -> Iterator[ssh_agent_client.types.KeyCommentPair]:
133)     """Yield all SSH keys suitable for passphrase derivation.
134) 
135)     Suitable SSH keys are queried from the running SSH agent (see
136)     [`ssh_agent_client.SSHAgentClient.list_keys`][]).
137) 
138)     Args:
139)         conn:
140)             An optional connection hint to the SSH agent; specifically,
141)             an SSH agent client, or a socket connected to an SSH agent.
142) 
143)             If an existing SSH agent client, then this client will be
144)             queried for the SSH keys, and otherwise left intact.
145) 
146)             If a socket, then a one-shot client will be constructed
147)             based on the socket to query the agent, and deconstructed
148)             afterwards.
149) 
150)             If neither are given, then the agent's socket location is
151)             looked up in the `SSH_AUTH_SOCK` environment variable, and
152)             used to construct/deconstruct a one-shot client, as in the
153)             previous case.
154) 
155)     Yields:
156)         :
157)             Every SSH key from the SSH agent that is suitable for
158)             passphrase derivation.
159) 
160)     Raises:
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

166) 
167)     """
168)     client: ssh_agent_client.SSHAgentClient
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

170)     match conn:
171)         case ssh_agent_client.SSHAgentClient():
172)             client = conn
173)             client_context = contextlib.nullcontext()
174)         case socket.socket() | None:
175)             client = ssh_agent_client.SSHAgentClient(socket=conn)
176)             client_context = client
177)         case _:  # pragma: no cover
178)             assert_never(conn)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

181)     with client_context:
182)         try:
183)             all_key_comment_pairs = list(client.list_keys())
184)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

188)         key, _comment = pair
189)         if dpp.Vault._is_suitable_ssh_key(key):  # noqa: SLF001
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

190)             yield pair
191)     if not suitable_keys:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

192)         raise IndexError(_NO_USABLE_KEYS)
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

193) 
194) 
195) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

238)             err=True,
239)             type=choices,
240)             show_choices=False,
241)             show_default=False,
242)             default='',
243)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

247)     prompt_suffix = (
248)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
249)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

250)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

251)         click.confirm(
252)             single_choice_prompt,
253)             prompt_suffix=prompt_suffix,
254)             err=True,
255)             abort=True,
256)             default=False,
257)             show_default=False,
258)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

259)     except click.Abort:
260)         raise IndexError(_EMPTY_SELECTION) from None
261)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

262) 
263) 
264) def _select_ssh_key(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

266) ) -> bytes | bytearray:
267)     """Interactively select an SSH key for passphrase derivation.
268) 
269)     Suitable SSH keys are queried from the running SSH agent (see
270)     [`ssh_agent_client.SSHAgentClient.list_keys`][]), then the user is
271)     prompted interactively (see [`click.prompt`][]) for a selection.
272) 
273)     Args:
274)         conn:
275)             An optional connection hint to the SSH agent; specifically,
276)             an SSH agent client, or a socket connected to an SSH agent.
277) 
278)             If an existing SSH agent client, then this client will be
279)             queried for the SSH keys, and otherwise left intact.
280) 
281)             If a socket, then a one-shot client will be constructed
282)             based on the socket to query the agent, and deconstructed
283)             afterwards.
284) 
285)             If neither are given, then the agent's socket location is
286)             looked up in the `SSH_AUTH_SOCK` environment variable, and
287)             used to construct/deconstruct a one-shot client, as in the
288)             previous case.
289) 
290)     Returns:
291)         The selected SSH key.
292) 
293)     Raises:
294)         IndexError:
295)             The user made an invalid or empty selection, or requested an
296)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

302)     """
303)     suitable_keys = list(_get_suitable_ssh_keys(conn))
304)     key_listing: list[str] = []
305)     unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix
306)     for key, comment in suitable_keys:
307)         keytype = unstring_prefix(key)[0].decode('ASCII')
308)         key_str = base64.standard_b64encode(key).decode('ASCII')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

309)         key_prefix = (
310)             key_str
311)             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
312)             else key_str[:KEY_DISPLAY_LENGTH] + '...'
313)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

317)         key_listing,
318)         heading='Suitable SSH keys:',
319)         single_choice_prompt='Use this key?',
320)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

321)     return suitable_keys[choice].key
322) 
323) 
324) def _prompt_for_passphrase() -> str:
325)     """Interactively prompt for the passphrase.
326) 
327)     Calls [`click.prompt`][] internally.  Moved into a separate function
328)     mainly for testing/mocking purposes.
329) 
330)     Returns:
331)         The user input.
332) 
333)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

334)     return cast(
335)         str,
336)         click.prompt(
337)             'Passphrase',
338)             default='',
339)             hide_input=True,
340)             show_default=False,
341)             err=True,
342)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

344) 
345) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

347)     """A [`click.Option`][] with an associated group name and group epilog.
348) 
349)     Used by [`derivepassphrase.cli.CommandWithHelpGroups`][] to print
350)     help sections.  Each subclass contains its own group name and
351)     epilog.
352) 
353)     Attributes:
354)         option_group_name:
355)             The name of the option group.  Used as a heading on the help
356)             text for options in this section.
357)         epilog:
358)             An epilog to print after listing the options in this
359)             section.
360) 
361)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

362) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

363)     option_group_name: str = ''
364)     epilog: str = ''
365) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

371) 
372) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

373)     """A [`click.Command`][] with support for help/option groups.
374) 
375)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
376)     further modified to support group epilogs.
377) 
378)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
379) 
380)     """
381) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

382)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

383)         self,
384)         ctx: click.Context,
385)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

389)         This is a callback for [`click.Command.get_help`][] that
390)         implements the `--help` listing, by calling appropriate methods
391)         of the `formatter`.  We list all options (like the base
392)         implementation), but grouped into sections according to the
393)         concrete [`click.Option`][] subclass being used.  If the option
394)         is an instance of some subclass `X` of
395)         [`derivepassphrase.cli.OptionGroupOption`][], then the section
396)         heading and the epilog are taken from `X.option_group_name` and
397)         `X.epilog`; otherwise, the section heading is "Options" (or
398)         "Other options" if there are other option groups) and the epilog
399)         is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

400) 
401)         Args:
402)             ctx:
403)                 The click context.
404)             formatter:
405)                 The formatter for the `--help` listing.
406) 
Marco Ricci Add minor documentation rew...

Marco Ricci authored 2 months ago

407)         Returns:
408)             Nothing.  Output is generated by calling appropriate methods
409)             on `formatter` instead.
410) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

412)         help_records: dict[str, list[tuple[str, str]]] = {}
413)         epilogs: dict[str, str] = {}
414)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

415)         if (  # pragma: no branch
416)             (help_opt := self.get_help_option(ctx)) is not None
417)             and help_opt not in params
418)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

419)             params.append(help_opt)
420)         for param in params:
421)             rec = param.get_help_record(ctx)
422)             if rec is not None:
423)                 if isinstance(param, OptionGroupOption):
424)                     group_name = param.option_group_name
425)                     epilogs.setdefault(group_name, param.epilog)
426)                 else:
427)                     group_name = ''
428)                 help_records.setdefault(group_name, []).append(rec)
429)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

430)         default_group_name = (
431)             'Other Options' if len(default_group) > 1 else 'Options'
432)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

433)         help_records[default_group_name] = default_group
434)         for group_name, records in help_records.items():
435)             with formatter.section(group_name):
436)                 formatter.write_dl(records)
437)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
438)             if epilog:
439)                 formatter.write_paragraph()
440)                 with formatter.indentation():
441)                     formatter.write_text(epilog)
442) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

454) 
455) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

463) 
464) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

471)     """
472) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

473) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

474) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

475)     ctx: click.Context,
476)     param: click.Parameter,
477)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

482)     if value is None:
483)         return value
484)     if isinstance(value, int):
485)         int_value = value
486)     else:
487)         try:
488)             int_value = int(value, 10)
489)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

495)     return int_value
496) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

497) 
498) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

499)     ctx: click.Context,
500)     param: click.Parameter,
501)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

506)     if value is None:
507)         return value
508)     if isinstance(value, int):
509)         int_value = value
510)     else:
511)         try:
512)             int_value = int(value, 10)
513)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

519)     return int_value
520) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

521) 
522) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

523) # Enter notes below the line with the cut mark (ASCII scissors and
524) # dashes).  Lines above the cut mark (such as this one) will be ignored.
525) #
526) # If you wish to clear the notes, leave everything beyond the cut mark
527) # blank.  However, if you leave the *entire* file blank, also removing
528) # the cut mark, then the edit is aborted, and the old notes contents are
529) # retained.
530) #
531) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

533) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
534) 
535) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

545) 
546)         Configuration is stored in a directory according to the
547)         DERIVEPASSPHRASE_PATH variable, which defaults to
548)         `~/.derivepassphrase` on UNIX-like systems and
549)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
550)         The configuration is NOT encrypted, and you are STRONGLY
551)         discouraged from using a stored passphrase.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

685) @click.argument('service', required=False)
686) @click.pass_context
687) def derivepassphrase(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

688)     ctx: click.Context,
689)     /,
690)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

691)     service: str | None = None,
692)     use_phrase: bool = False,
693)     use_key: bool = False,
694)     length: int | None = None,
695)     repeat: int | None = None,
696)     lower: int | None = None,
697)     upper: int | None = None,
698)     number: int | None = None,
699)     space: int | None = None,
700)     dash: int | None = None,
701)     symbol: int | None = None,
702)     edit_notes: bool = False,
703)     store_config_only: bool = False,
704)     delete_service_settings: bool = False,
705)     delete_globals: bool = False,
706)     clear_all_settings: bool = False,
707)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
708)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

709) ) -> None:
710)     """Derive a strong passphrase, deterministically, from a master secret.
711) 
Marco Ricci Fill out README and documen...

Marco Ricci authored 2 months ago

712)     Using a master passphrase or a master SSH key, derive a passphrase
713)     for SERVICE, subject to length, character and character repetition
714)     constraints.  The derivation is cryptographically strong, meaning
715)     that even if a single passphrase is compromised, guessing the master
716)     passphrase or a different service's passphrase is computationally
717)     infeasible.  The derivation is also deterministic, given the same
718)     inputs, thus the resulting passphrase need not be stored explicitly.
719)     The service name and constraints themselves also need not be kept
720)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

730) 
731)     [CLICK]: https://click.palletsprojects.com/
732) 
733)     Parameters:
734)         ctx (click.Context):
735)             The `click` context.
736) 
737)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

790)         export_settings:
791)             Command-line argument `-e`/`--export`.  If a file object,
792)             then it must be open for writing and accept `str` inputs.
793)             Otherwise, a filename to open for writing.  Using `-` for
794)             standard output is supported.
795)         import_settings:
796)             Command-line argument `-i`/`--import`.  If a file object, it
797)             must be open for reading and yield `str` values.  Otherwise,
798)             a filename to open for reading.  Using `-` for standard
799)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

800) 
801)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

808)             match param:
809)                 case PasswordGenerationOption():
810)                     group = PasswordGenerationOption
811)                 case ConfigurationOption():
812)                     group = ConfigurationOption
813)                 case StorageManagementOption():
814)                     group = StorageManagementOption
815)                 case OptionGroupOption():
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

819)                 case _:
820)                     group = click.Option
821)             options_in_group.setdefault(group, []).append(param)
822)         params_by_str[param.human_readable_name] = param
823)         for name in param.opts + param.secondary_opts:
824)             params_by_str[name] = param
825) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

829)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

833)         if isinstance(param, str):
834)             param = params_by_str[param]
835)         assert isinstance(param, click.Parameter)
836)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

837)             return
838)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

841)             assert isinstance(other, click.Parameter)
842)             if other != param and is_param_set(other):
843)                 opt_str = param.opts[0]
844)                 other_str = other.opts[0]
845)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

848) 
849)     def get_config() -> dpp_types.VaultConfig:
850)         try:
851)             return _load_config()
852)         except FileNotFoundError:
853)             return {'services': {}}
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

854)         except Exception as e:  # noqa: BLE001
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

855)             ctx.fail(f'cannot load config: {e}')
856) 
857)     configuration: dpp_types.VaultConfig
858) 
859)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

864)                     opt, *options_in_group[PasswordGenerationOption]
865)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

870)                 opt,
871)                 *options_in_group[ConfigurationOption],
872)                 *options_in_group[StorageManagementOption],
873)             )
874)     sv_options = options_in_group[PasswordGenerationOption] + [
875)         params_by_str['--notes'],
876)         params_by_str['--delete'],
877)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

878)     sv_options.remove(params_by_str['--key'])
879)     sv_options.remove(params_by_str['--phrase'])
880)     for param in sv_options:
881)         if is_param_set(param) and not service:
882)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

892)     no_sv_options = [
893)         params_by_str['--delete-globals'],
894)         params_by_str['--clear'],
895)         *options_in_group[StorageManagementOption],
896)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

902) 
903)     if edit_notes:
904)         assert service is not None
905)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

906)         text = DEFAULT_NOTES_TEMPLATE + configuration['services'].get(
907)             service, cast(dpp_types.VaultConfigServicesSettings, {})
908)         ).get('notes', '')
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

909)         notes_value = click.edit(text=text)
910)         if notes_value is not None:
911)             notes_lines = collections.deque(notes_value.splitlines(True))
912)             while notes_lines:
913)                 line = notes_lines.popleft()
914)                 if line.startswith(DEFAULT_NOTES_MARKER):
915)                     notes_value = ''.join(notes_lines)
916)                     break
917)             else:
918)                 if not notes_value.strip():
919)                     ctx.fail('not saving new notes: user aborted request')
920)             configuration['services'].setdefault(service, {})['notes'] = (
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

921)                 notes_value.strip('\n')
922)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

923)             _save_config(configuration)
924)     elif delete_service_settings:
925)         assert service is not None
926)         configuration = get_config()
927)         if service in configuration['services']:
928)             del configuration['services'][service]
929)             _save_config(configuration)
930)     elif delete_globals:
931)         configuration = get_config()
932)         if 'global' in configuration:
933)             del configuration['global']
934)             _save_config(configuration)
935)     elif clear_all_settings:
936)         _save_config({'services': {}})
937)     elif import_settings:
938)         try:
939)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

940)             infile = (
941)                 cast(TextIO, import_settings)
942)                 if hasattr(import_settings, 'close')
943)                 else click.open_file(os.fspath(import_settings), 'rt')
944)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

945)             with infile:
946)                 maybe_config = json.load(infile)
947)         except json.JSONDecodeError as e:
948)             ctx.fail(f'Cannot load config: cannot decode JSON: {e}')
949)         except OSError as e:
950)             ctx.fail(f'Cannot load config: {e.strerror}')
951)         if dpp_types.is_vault_config(maybe_config):
952)             _save_config(maybe_config)
953)         else:
954)             ctx.fail('not a valid config')
955)     elif export_settings:
956)         configuration = get_config()
957)         try:
958)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

959)             outfile = (
960)                 cast(TextIO, export_settings)
961)                 if hasattr(export_settings, 'close')
962)                 else click.open_file(os.fspath(export_settings), 'wt')
963)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

964)             with outfile:
965)                 json.dump(configuration, outfile)
966)         except OSError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 2 months ago

967)             ctx.fail(f'cannot write config: {e.strerror}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

975)         service_keys = {
976)             'key',
977)             'phrase',
978)             'length',
979)             'repeat',
980)             'lower',
981)             'upper',
982)             'number',
983)             'space',
984)             'dash',
985)             'symbol',
986)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

988)             {
989)                 k: v
990)                 for k, v in locals().items()
991)                 if k in service_keys and v is not None
992)             },
993)             cast(
994)                 dict[str, Any],
995)                 configuration['services'].get(service or '', {}),
996)             ),
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

997)             {},
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

999)         )
1000)         if use_key:
1001)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1002)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1003)                     'ASCII'
1004)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1005)             except IndexError:
1006)                 ctx.fail('no valid SSH key selected')
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

1007)             except (LookupError, RuntimeError) as e:
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1008)                 ctx.fail(str(e))
1009)         elif use_phrase:
1010)             maybe_phrase = _prompt_for_passphrase()
1011)             if not maybe_phrase:
1012)                 ctx.fail('no passphrase given')
1013)             else:
1014)                 phrase = maybe_phrase
1015)         if store_config_only:
1016)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1017)             view = (
1018)                 collections.ChainMap(*settings.maps[:2])
1019)                 if service
1020)                 else settings.parents.parents
1021)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1022)             if use_key:
1023)                 view['key'] = key
1024)                 for m in view.maps:
1025)                     m.pop('phrase', '')
1026)             elif use_phrase:
1027)                 view['phrase'] = phrase
1028)                 for m in view.maps:
1029)                     m.pop('key', '')
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1032)                 msg = (
1033)                     f'cannot update {settings_type} settings without '
1034)                     f'actual settings'
1035)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1037)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1039)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1040)                 configuration.setdefault('global', {}).update(view)  # type: ignore[typeddict-item]
1041)             assert dpp_types.is_vault_config(
1042)                 configuration
1043)             ), f'invalid vault configuration: {configuration!r}'
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1044)             _save_config(configuration)
1045)         else:
1046)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1049)             kwargs: dict[str, Any] = {
1050)                 k: v
1051)                 for k, v in settings.items()
1052)                 if k in service_keys and v is not None
1053)             }
1054) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 1 month ago

1055)             def key_to_phrase(
1056)                 key: str | bytes | bytearray,
1057)             ) -> bytes | bytearray:
1058)                 return dpp.Vault.phrase_from_key(
1059)                     base64.standard_b64decode(key)
1060)                 )
1061) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1062)             # If either --key or --phrase are given, use that setting.
1063)             # Otherwise, if both key and phrase are set in the config,
1064)             # one must be global (ignore it) and one must be
1065)             # service-specific (use that one). Otherwise, if only one of
1066)             # key and phrase is set in the config, use that one.  In all
1067)             # these above cases, set the phrase via
1068)             # derivepassphrase.Vault.phrase_from_key if a key is
1069)             # given. Finally, if nothing is set, error out.
1070)             if use_key or use_phrase:
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1077)             elif kwargs.get('phrase'):
1078)                 pass
1079)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1080)                 msg = (
1081)                     'no passphrase or key given on command-line '
1082)                     'or in configuration'
1083)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1085)             kwargs.pop('key', '')
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1086)             vault = dpp.Vault(**kwargs)
1087)             result = vault.generate(service)
1088)             click.echo(result.decode('ASCII'))