8c9ca812be07a78de272aad82c08251cbaae5164
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 Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

164) 
165)     """
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

170)             client = conn
171)             client_context = contextlib.nullcontext()
172)         case socket.socket() | None:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

174)             client_context = client
175)         case _:  # pragma: no cover
176)             assert_never(conn)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

191) 
192) 
193) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

248)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

260) 
261) 
262) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

300)     """
301)     suitable_keys = list(_get_suitable_ssh_keys(conn))
302)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

342) 
343) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

360) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

369) 
370) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

380)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

452) 
453) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

461) 
462) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

469)     """
470) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

471) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

472) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

493)     return int_value
494) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

495) 
496) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

517)     return int_value
518) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

519) 
520) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

798) 
799)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

827)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

835)             return
836)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

848)         try:
849)             return _load_config()
850)         except FileNotFoundError:
851)             return {'services': {}}
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

853)             ctx.fail(f'cannot load config: {e}')
854) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

856) 
857)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

862)                     opt, *options_in_group[PasswordGenerationOption]
863)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

943)             with infile:
944)                 maybe_config = json.load(infile)
945)         except json.JSONDecodeError as e:
946)             ctx.fail(f'Cannot load config: cannot decode JSON: {e}')
947)         except OSError as e:
948)             ctx.fail(f'Cannot load config: {e.strerror}')
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

949)         if _types.is_vault_config(maybe_config):
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

950)             _save_config(maybe_config)
951)         else:
952)             ctx.fail('not a valid config')
953)     elif export_settings:
954)         configuration = get_config()
955)         try:
956)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

995)             {},
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

997)         )
998)         if use_key:
999)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1035)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1037)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1053)             def key_to_phrase(
1054)                 key: str | bytes | bytearray,
1055)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1057)                     base64.standard_b64decode(key)
1058)                 )
1059) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1060)             # If either --key or --phrase are given, use that setting.
1061)             # Otherwise, if both key and phrase are set in the config,
1062)             # one must be global (ignore it) and one must be
1063)             # service-specific (use that one). Otherwise, if only one of
1064)             # key and phrase is set in the config, use that one.  In all
1065)             # these above cases, set the phrase via
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1073)             elif kwargs.get('key'):
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('phrase'):
1076)                 pass
1077)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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