fc8c8f924a2a6876f3f954579e2ad170834a71de
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:
100)     """Save a vault(1)-compatbile config to the application directory.
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 Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

121)         json.dump(config, fileobj)
122) 
123) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

124) def _get_suitable_ssh_keys(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

160) 
161)     """
162)     client: ssh_agent_client.SSHAgentClient
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

164)     match conn:
165)         case ssh_agent_client.SSHAgentClient():
166)             client = conn
167)             client_context = contextlib.nullcontext()
168)         case socket.socket() | None:
169)             client = ssh_agent_client.SSHAgentClient(socket=conn)
170)             client_context = client
171)         case _:  # pragma: no cover
172)             assert_never(conn)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

175)     with client_context:
176)         try:
177)             all_key_comment_pairs = list(client.list_keys())
178)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

187) 
188) 
189) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

232)             err=True,
233)             type=choices,
234)             show_choices=False,
235)             show_default=False,
236)             default='',
237)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

241)     prompt_suffix = (
242)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
243)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

244)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

245)         click.confirm(
246)             single_choice_prompt,
247)             prompt_suffix=prompt_suffix,
248)             err=True,
249)             abort=True,
250)             default=False,
251)             show_default=False,
252)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

253)     except click.Abort:
254)         raise IndexError(_EMPTY_SELECTION) from None
255)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

256) 
257) 
258) def _select_ssh_key(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

296)     """
297)     suitable_keys = list(_get_suitable_ssh_keys(conn))
298)     key_listing: list[str] = []
299)     unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix
300)     for key, comment in suitable_keys:
301)         keytype = unstring_prefix(key)[0].decode('ASCII')
302)         key_str = base64.standard_b64encode(key).decode('ASCII')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

303)         key_prefix = (
304)             key_str
305)             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
306)             else key_str[:KEY_DISPLAY_LENGTH] + '...'
307)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

311)         key_listing,
312)         heading='Suitable SSH keys:',
313)         single_choice_prompt='Use this key?',
314)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

328)     return cast(
329)         str,
330)         click.prompt(
331)             'Passphrase',
332)             default='',
333)             hide_input=True,
334)             show_default=False,
335)             err=True,
336)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

338) 
339) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

356) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

357)     option_group_name: str = ''
358)     epilog: str = ''
359) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

365) 
366) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

376)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

377)         self,
378)         ctx: click.Context,
379)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

394) 
395)         Args:
396)             ctx:
397)                 The click context.
398)             formatter:
399)                 The formatter for the `--help` listing.
400) 
Marco Ricci Add minor documentation rew...

Marco Ricci authored 2 months ago

401)         Returns:
402)             Nothing.  Output is generated by calling appropriate methods
403)             on `formatter` instead.
404) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

406)         help_records: dict[str, list[tuple[str, str]]] = {}
407)         epilogs: dict[str, str] = {}
408)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

409)         if (  # pragma: no branch
410)             (help_opt := self.get_help_option(ctx)) is not None
411)             and help_opt not in params
412)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

413)             params.append(help_opt)
414)         for param in params:
415)             rec = param.get_help_record(ctx)
416)             if rec is not None:
417)                 if isinstance(param, OptionGroupOption):
418)                     group_name = param.option_group_name
419)                     epilogs.setdefault(group_name, param.epilog)
420)                 else:
421)                     group_name = ''
422)                 help_records.setdefault(group_name, []).append(rec)
423)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

424)         default_group_name = (
425)             'Other Options' if len(default_group) > 1 else 'Options'
426)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

427)         help_records[default_group_name] = default_group
428)         for group_name, records in help_records.items():
429)             with formatter.section(group_name):
430)                 formatter.write_dl(records)
431)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
432)             if epilog:
433)                 formatter.write_paragraph()
434)                 with formatter.indentation():
435)                     formatter.write_text(epilog)
436) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

448) 
449) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

457) 
458) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

467) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

468) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

469)     ctx: click.Context,
470)     param: click.Parameter,
471)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

476)     if value is None:
477)         return value
478)     if isinstance(value, int):
479)         int_value = value
480)     else:
481)         try:
482)             int_value = int(value, 10)
483)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

489)     return int_value
490) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

491) 
492) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

493)     ctx: click.Context,
494)     param: click.Parameter,
495)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

500)     if value is None:
501)         return value
502)     if isinstance(value, int):
503)         int_value = value
504)     else:
505)         try:
506)             int_value = int(value, 10)
507)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

513)     return int_value
514) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

515) 
516) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

527) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
528) 
529) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

679) @click.argument('service', required=False)
680) @click.pass_context
681) def derivepassphrase(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

682)     ctx: click.Context,
683)     /,
684)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

703) ) -> None:
704)     """Derive a strong passphrase, deterministically, from a master secret.
705) 
Marco Ricci Fill out README and documen...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

724) 
725)     [CLICK]: https://click.palletsprojects.com/
726) 
727)     Parameters:
728)         ctx (click.Context):
729)             The `click` context.
730) 
731)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

794) 
795)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

802)             match param:
803)                 case PasswordGenerationOption():
804)                     group = PasswordGenerationOption
805)                 case ConfigurationOption():
806)                     group = ConfigurationOption
807)                 case StorageManagementOption():
808)                     group = StorageManagementOption
809)                 case OptionGroupOption():
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

813)                 case _:
814)                     group = click.Option
815)             options_in_group.setdefault(group, []).append(param)
816)         params_by_str[param.human_readable_name] = param
817)         for name in param.opts + param.secondary_opts:
818)             params_by_str[name] = param
819) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

823)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

827)         if isinstance(param, str):
828)             param = params_by_str[param]
829)         assert isinstance(param, click.Parameter)
830)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

831)             return
832)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

835)             assert isinstance(other, click.Parameter)
836)             if other != param and is_param_set(other):
837)                 opt_str = param.opts[0]
838)                 other_str = other.opts[0]
839)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

842) 
843)     def get_config() -> dpp_types.VaultConfig:
844)         try:
845)             return _load_config()
846)         except FileNotFoundError:
847)             return {'services': {}}
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

849)             ctx.fail(f'cannot load config: {e}')
850) 
851)     configuration: dpp_types.VaultConfig
852) 
853)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

858)                     opt, *options_in_group[PasswordGenerationOption]
859)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

864)                 opt,
865)                 *options_in_group[ConfigurationOption],
866)                 *options_in_group[StorageManagementOption],
867)             )
868)     sv_options = options_in_group[PasswordGenerationOption] + [
869)         params_by_str['--notes'],
870)         params_by_str['--delete'],
871)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

872)     sv_options.remove(params_by_str['--key'])
873)     sv_options.remove(params_by_str['--phrase'])
874)     for param in sv_options:
875)         if is_param_set(param) and not service:
876)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

886)     no_sv_options = [
887)         params_by_str['--delete-globals'],
888)         params_by_str['--clear'],
889)         *options_in_group[StorageManagementOption],
890)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

896) 
897)     if edit_notes:
898)         assert service is not None
899)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

934)             infile = (
935)                 cast(TextIO, import_settings)
936)                 if hasattr(import_settings, 'close')
937)                 else click.open_file(os.fspath(import_settings), 'rt')
938)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

953)             outfile = (
954)                 cast(TextIO, export_settings)
955)                 if hasattr(export_settings, 'close')
956)                 else click.open_file(os.fspath(export_settings), 'wt')
957)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

958)             with outfile:
959)                 json.dump(configuration, outfile)
960)         except OSError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

969)         service_keys = {
970)             'key',
971)             'phrase',
972)             'length',
973)             'repeat',
974)             'lower',
975)             'upper',
976)             'number',
977)             'space',
978)             'dash',
979)             'symbol',
980)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

982)             {
983)                 k: v
984)                 for k, v in locals().items()
985)                 if k in service_keys and v is not None
986)             },
987)             cast(
988)                 dict[str, Any],
989)                 configuration['services'].get(service or '', {}),
990)             ),
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

991)             {},
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

993)         )
994)         if use_key:
995)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

996)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
997)                     'ASCII'
998)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

1002)                 ctx.fail(str(e))
1003)         elif use_phrase:
1004)             maybe_phrase = _prompt_for_passphrase()
1005)             if not maybe_phrase:
1006)                 ctx.fail('no passphrase given')
1007)             else:
1008)                 phrase = maybe_phrase
1009)         if store_config_only:
1010)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1011)             view = (
1012)                 collections.ChainMap(*settings.maps[:2])
1013)                 if service
1014)                 else settings.parents.parents
1015)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1016)             if use_key:
1017)                 view['key'] = key
1018)                 for m in view.maps:
1019)                     m.pop('phrase', '')
1020)             elif use_phrase:
1021)                 view['phrase'] = phrase
1022)                 for m in view.maps:
1023)                     m.pop('key', '')
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1026)                 msg = (
1027)                     f'cannot update {settings_type} settings without '
1028)                     f'actual settings'
1029)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1031)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1033)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1038)             _save_config(configuration)
1039)         else:
1040)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1043)             kwargs: dict[str, Any] = {
1044)                 k: v
1045)                 for k, v in settings.items()
1046)                 if k in service_keys and v is not None
1047)             }
1048) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1049)             # If either --key or --phrase are given, use that setting.
1050)             # Otherwise, if both key and phrase are set in the config,
1051)             # one must be global (ignore it) and one must be
1052)             # service-specific (use that one). Otherwise, if only one of
1053)             # key and phrase is set in the config, use that one.  In all
1054)             # these above cases, set the phrase via
1055)             # derivepassphrase.Vault.phrase_from_key if a key is
1056)             # given. Finally, if nothing is set, error out.
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 2 months ago

1057)             def key_to_phrase(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1058)                 key: str | bytes | bytearray,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 2 months ago

1059)             ) -> bytes | bytearray:
1060)                 return dpp.Vault.phrase_from_key(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1061)                     base64.standard_b64decode(key)
1062)                 )
1063) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1064)             if use_key or use_phrase:
1065)                 if use_key:
1066)                     kwargs['phrase'] = key_to_phrase(key)
1067)                 else:
1068)                     kwargs['phrase'] = phrase
1069)                     kwargs.pop('key', '')
1070)             elif kwargs.get('phrase') and kwargs.get('key'):
1071)                 if any('key' in m for m in settings.maps[:2]):
1072)                     kwargs['phrase'] = key_to_phrase(kwargs.pop('key'))
1073)                 else:
1074)                     kwargs.pop('key')
1075)             elif kwargs.get('key'):
1076)                 kwargs['phrase'] = key_to_phrase(kwargs.pop('key'))
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 Add finished command-line i...

Marco Ricci authored 2 months ago

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