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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2) #
3) # SPDX-License-Identifier: MIT
4) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

5) # ruff: noqa: TRY400
6) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

8) 
9) from __future__ import annotations
10) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

11) import base64
12) import collections
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

16) import inspect
17) import json
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

20) import unicodedata
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

21) import warnings
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

22) from typing import (
23)     TYPE_CHECKING,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

24)     Callable,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

27)     TextIO,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

28)     TypeVar,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

29)     cast,
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

31) 
32) import click
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

33) from typing_extensions import (
34)     Any,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

35)     ParamSpec,
36)     Self,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

37)     assert_never,
38) )
39) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

42) 
43) if TYPE_CHECKING:
44)     import pathlib
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

47)     from collections.abc import (
48)         Iterator,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

49)         MutableSequence,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

50)         Sequence,
51)     )
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

52) 
53) __author__ = dpp.__author__
54) __version__ = dpp.__version__
55) 
56) __all__ = ('derivepassphrase',)
57) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 5 months ago

60) 
61) # Error messages
62) _INVALID_VAULT_CONFIG = 'Invalid vault config'
63) _AGENT_COMMUNICATION_ERROR = 'Error communicating with the SSH agent'
64) _NO_USABLE_KEYS = 'No usable SSH keys were found'
65) _EMPTY_SELECTION = 'Empty selection'
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

66) 
67) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

68) # Logging
69) # =======
70) 
71) 
72) class ClickEchoStderrHandler(logging.Handler):
73)     """A [`logging.Handler`][] for `click` applications.
74) 
75)     Outputs log messages to [`sys.stderr`][] via [`click.echo`][].
76) 
77)     """
78) 
79)     def emit(self, record: logging.LogRecord) -> None:
80)         """Emit a log record.
81) 
82)         Format the log record, then emit it via [`click.echo`][] to
83)         [`sys.stderr`][].
84) 
85)         """
86)         click.echo(self.format(record), err=True)
87) 
88) 
89) class CLIofPackageFormatter(logging.Formatter):
90)     """A [`logging.LogRecord`][] formatter for the CLI of a Python package.
91) 
92)     Assuming a package `PKG` and loggers within the same hierarchy
93)     `PKG`, format all log records from that hierarchy for proper user
94)     feedback on the console.  Intended for use with [`click`][CLICK] and
95)     when `PKG` provides a command-line tool `PKG` and when logs from
96)     that package should show up as output of the command-line tool.
97) 
98)     Essentially, this prepends certain short strings to the log message
99)     lines to make them readable as standard error output.
100) 
101)     Because this log output is intended to be displayed on standard
102)     error as high-level diagnostic output, you are strongly discouraged
103)     from changing the output format to include more tokens besides the
104)     log message.  Use a dedicated log file handler instead, without this
105)     formatter.
106) 
107)     [CLICK]: https://pypi.org/projects/click/
108) 
109)     """
110) 
111)     def __init__(
112)         self,
113)         *,
114)         prog_name: str = PROG_NAME,
115)         package_name: str | None = None,
116)     ) -> None:
117)         self.prog_name = prog_name
118)         self.package_name = (
119)             package_name
120)             if package_name is not None
121)             else prog_name.lower().replace(' ', '_').replace('-', '_')
122)         )
123) 
124)     def format(self, record: logging.LogRecord) -> str:
125)         """Format a log record suitably for standard error console output.
126) 
127)         Prepend the formatted string `"PROG_NAME: LABEL"` to each line
128)         of the message, where `PROG_NAME` is the program name, and
129)         `LABEL` depends on the record's level and on the logger name as
130)         follows:
131) 
132)           * For records at level [`logging.DEBUG`][], `LABEL` is
133)             `"Debug: "`.
134)           * For records at level [`logging.INFO`][], `LABEL` is the
135)             empty string.
136)           * For records at level [`logging.WARNING`][], `LABEL` is
137)             `"Deprecation warning: "` if the logger is named
138)             `PKG.deprecation` (where `PKG` is the package name), else
139)             `"Warning: "`.
140)           * For records at level [`logging.ERROR`][] and
141)             [`logging.CRITICAL`][] `"Error: "`, `LABEL` is `"ERROR: "`.
142) 
143)         The level indication strings at level `WARNING` or above are
144)         highlighted.  Use [`click.echo`][] to output them and remove
145)         color output if necessary.
146) 
147)         Args:
148)             record: A log record.
149) 
150)         Returns:
151)             A formatted log record.
152) 
153)         Raises:
154)             AssertionError:
155)                 The log level is not supported.
156) 
157)         """
158)         preliminary_result = record.getMessage()
159)         prefix = f'{self.prog_name}: '
160)         if record.levelname == 'DEBUG':  # pragma: no cover
161)             level_indicator = 'Debug: '
162)         elif record.levelname == 'INFO':
163)             level_indicator = ''
164)         elif record.levelname == 'WARNING':
165)             level_indicator = (
166)                 f'{click.style("Deprecation warning", bold=True)}: '
167)                 if record.name.endswith('.deprecation')
168)                 else f'{click.style("Warning", bold=True)}: '
169)             )
170)         elif record.levelname in {'ERROR', 'CRITICAL'}:
171)             level_indicator = ''
172)         else:  # pragma: no cover
173)             msg = f'Unsupported logging level: {record.levelname}'
174)             raise AssertionError(msg)
175)         return ''.join(
176)             prefix + level_indicator + line
177)             for line in preliminary_result.splitlines(True)  # noqa: FBT003
178)         )
179) 
180) 
181) class StandardCLILogging:
182)     """Set up CLI logging handlers upon instantiation."""
183) 
184)     prog_name = PROG_NAME
185)     package_name = PROG_NAME.lower().replace(' ', '_').replace('-', '_')
186)     cli_formatter = CLIofPackageFormatter(
187)         prog_name=prog_name, package_name=package_name
188)     )
189)     cli_handler = ClickEchoStderrHandler()
190)     cli_handler.addFilter(logging.Filter(name=package_name))
191)     cli_handler.setFormatter(cli_formatter)
192)     cli_handler.setLevel(logging.WARNING)
193)     warnings_handler = ClickEchoStderrHandler()
194)     warnings_handler.addFilter(logging.Filter(name='py.warnings'))
195)     warnings_handler.setFormatter(cli_formatter)
196)     warnings_handler.setLevel(logging.WARNING)
197) 
198)     @classmethod
199)     def ensure_standard_logging(cls) -> StandardLoggingContextManager:
200)         """Return a context manager to ensure standard logging is set up."""
201)         return StandardLoggingContextManager(
202)             handler=cls.cli_handler,
203)             root_logger=cls.package_name,
204)         )
205) 
206)     @classmethod
207)     def ensure_standard_warnings_logging(
208)         cls,
209)     ) -> StandardWarningsLoggingContextManager:
210)         """Return a context manager to ensure warnings logging is set up."""
211)         return StandardWarningsLoggingContextManager(
212)             handler=cls.warnings_handler,
213)         )
214) 
215) 
216) class StandardLoggingContextManager:
217)     """A reentrant context manager setting up standard CLI logging.
218) 
219)     Ensures that the given handler (defaulting to the CLI logging
220)     handler) is added to the named logger (defaulting to the root
221)     logger), and if it had to be added, then that it will be removed
222)     upon exiting the context.
223) 
224)     Reentrant, but not thread safe, because it temporarily modifies
225)     global state.
226) 
227)     """
228) 
229)     def __init__(
230)         self,
231)         handler: logging.Handler,
232)         root_logger: str | None = None,
233)     ) -> None:
234)         self.handler = handler
235)         self.root_logger_name = root_logger
236)         self.base_logger = logging.getLogger(self.root_logger_name)
237)         self.action_required: MutableSequence[bool] = collections.deque()
238) 
239)     def __enter__(self) -> Self:
240)         self.action_required.append(
241)             self.handler not in self.base_logger.handlers
242)         )
243)         if self.action_required[-1]:
244)             self.base_logger.addHandler(self.handler)
245)         return self
246) 
247)     def __exit__(
248)         self,
249)         exc_type: type[BaseException] | None,
250)         exc_value: BaseException | None,
251)         exc_tb: types.TracebackType | None,
252)     ) -> Literal[False]:
253)         if self.action_required[-1]:
254)             self.base_logger.removeHandler(self.handler)
255)         self.action_required.pop()
256)         return False
257) 
258) 
259) class StandardWarningsLoggingContextManager(StandardLoggingContextManager):
260)     """A reentrant context manager setting up standard warnings logging.
261) 
262)     Ensures that warnings are being diverted to the logging system, and
263)     that the given handler (defaulting to the CLI logging handler) is
264)     added to the warnings logger. If the handler had to be added, then
265)     it will be removed upon exiting the context.
266) 
267)     Reentrant, but not thread safe, because it temporarily modifies
268)     global state.
269) 
270)     """
271) 
272)     def __init__(
273)         self,
274)         handler: logging.Handler,
275)     ) -> None:
276)         super().__init__(handler=handler, root_logger='py.warnings')
277)         self.stack: MutableSequence[
278)             tuple[
279)                 Callable[
280)                     [
281)                         type[BaseException] | None,
282)                         BaseException | None,
283)                         types.TracebackType | None,
284)                     ],
285)                     None,
286)                 ],
287)                 Callable[
288)                     [
289)                         str | Warning,
290)                         type[Warning],
291)                         str,
292)                         int,
293)                         TextIO | None,
294)                         str | None,
295)                     ],
296)                     None,
297)                 ],
298)             ]
299)         ] = collections.deque()
300) 
301)     def __enter__(self) -> Self:
302)         def showwarning(  # noqa: PLR0913,PLR0917
303)             message: str | Warning,
304)             category: type[Warning],
305)             filename: str,
306)             lineno: int,
307)             file: TextIO | None = None,
308)             line: str | None = None,
309)         ) -> None:
310)             if file is not None:  # pragma: no cover
311)                 self.stack[0][1](
312)                     message, category, filename, lineno, file, line
313)                 )
314)             else:
315)                 logging.getLogger('py.warnings').warning(
316)                     str(
317)                         warnings.formatwarning(
318)                             message, category, filename, lineno, line
319)                         )
320)                     )
321)                 )
322) 
323)         ctx = warnings.catch_warnings()
324)         exit_func = ctx.__exit__
325)         ctx.__enter__()
326)         self.stack.append((exit_func, warnings.showwarning))
327)         warnings.showwarning = showwarning
328)         return super().__enter__()
329) 
330)     def __exit__(
331)         self,
332)         exc_type: type[BaseException] | None,
333)         exc_value: BaseException | None,
334)         exc_tb: types.TracebackType | None,
335)     ) -> Literal[False]:
336)         ret = super().__exit__(exc_type, exc_value, exc_tb)
337)         val = self.stack.pop()[0](exc_type, exc_value, exc_tb)
338)         assert not val
339)         return ret
340) 
341) 
342) P = ParamSpec('P')
343) R = TypeVar('R')
344) 
345) 
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

346) def adjust_logging_level(
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

347)     ctx: click.Context,
348)     /,
349)     param: click.Parameter | None = None,
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

350)     value: int | None = None,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

351) ) -> None:
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

352)     """Change the logs that are emitted to standard error.
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

353) 
354)     This modifies the [`StandardCLILogging`][] settings such that log
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

355)     records at the respective level are emitted, based on the `param`
356)     and the `value`.
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

357) 
358)     """
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

359)     # Note: If multiple options use this callback, then we will be
360)     # called multiple times.  Ensure the runs are idempotent.
361)     if param is None or value is None or ctx.resilient_parsing:
362)         return
363)     StandardCLILogging.cli_handler.setLevel(value)
364)     logging.getLogger(StandardCLILogging.package_name).setLevel(value)
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

365) 
366) 
Marco Ricci Shift option parsing and gr...

Marco Ricci authored 1 month ago

367) # Option parsing and grouping
368) # ===========================
369) 
370) 
371) class OptionGroupOption(click.Option):
372)     """A [`click.Option`][] with an associated group name and group epilog.
373) 
374)     Used by [`CommandWithHelpGroups`][] to print help sections.  Each
375)     subclass contains its own group name and epilog.
376) 
377)     Attributes:
378)         option_group_name:
379)             The name of the option group.  Used as a heading on the help
380)             text for options in this section.
381)         epilog:
382)             An epilog to print after listing the options in this
383)             section.
384) 
385)     """
386) 
387)     option_group_name: str = ''
388)     """"""
389)     epilog: str = ''
390)     """"""
391) 
392)     def __init__(self, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401
393)         if self.__class__ == __class__:  # type: ignore[name-defined]
394)             raise NotImplementedError
395)         super().__init__(*args, **kwargs)
396) 
397) 
398) class CommandWithHelpGroups(click.Command):
399)     """A [`click.Command`][] with support for help/option groups.
400) 
401)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
402)     further modified to support group epilogs.
403) 
404)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
405) 
406)     """
407) 
408)     def format_options(
409)         self,
410)         ctx: click.Context,
411)         formatter: click.HelpFormatter,
412)     ) -> None:
413)         r"""Format options on the help listing, grouped into sections.
414) 
415)         This is a callback for [`click.Command.get_help`][] that
416)         implements the `--help` listing, by calling appropriate methods
417)         of the `formatter`.  We list all options (like the base
418)         implementation), but grouped into sections according to the
419)         concrete [`click.Option`][] subclass being used.  If the option
420)         is an instance of some subclass of [`OptionGroupOption`][], then
421)         the section heading and the epilog are taken from the
422)         [`option_group_name`] [OptionGroupOption.option_group_name] and
423)         [`epilog`] [OptionGroupOption.epilog] attributes; otherwise, the
424)         section heading is "Options" (or "Other options" if there are
425)         other option groups) and the epilog is empty.
426) 
427)         Args:
428)             ctx:
429)                 The click context.
430)             formatter:
431)                 The formatter for the `--help` listing.
432) 
433)         """
434)         help_records: dict[str, list[tuple[str, str]]] = {}
435)         epilogs: dict[str, str] = {}
436)         params = self.params[:]
437)         if (  # pragma: no branch
438)             (help_opt := self.get_help_option(ctx)) is not None
439)             and help_opt not in params
440)         ):
441)             params.append(help_opt)
442)         for param in params:
443)             rec = param.get_help_record(ctx)
444)             if rec is not None:
445)                 if isinstance(param, OptionGroupOption):
446)                     group_name = param.option_group_name
447)                     epilogs.setdefault(group_name, param.epilog)
448)                 else:
449)                     group_name = ''
450)                 help_records.setdefault(group_name, []).append(rec)
451)         default_group = help_records.pop('')
452)         default_group_name = (
453)             'Other Options' if len(default_group) > 1 else 'Options'
454)         )
455)         help_records[default_group_name] = default_group
456)         for group_name, records in help_records.items():
457)             with formatter.section(group_name):
458)                 formatter.write_dl(records)
459)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
460)             if epilog:
461)                 formatter.write_paragraph()
462)                 with formatter.indentation():
463)                     formatter.write_text(epilog)
464) 
465) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

466) class LoggingOption(OptionGroupOption):
467)     """Logging options for the CLI."""
468) 
469)     option_group_name = 'Logging'
470)     epilog = ''
471) 
472) 
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

473) debug_option = click.option(
474)     '--debug',
475)     'logging_level',
476)     is_flag=True,
477)     flag_value=logging.DEBUG,
478)     expose_value=False,
479)     callback=adjust_logging_level,
480)     help='also emit debug information (implies --verbose)',
481)     cls=LoggingOption,
482) )
483) verbose_option = click.option(
484)     '-v',
485)     '--verbose',
486)     'logging_level',
487)     is_flag=True,
488)     flag_value=logging.INFO,
489)     expose_value=False,
490)     callback=adjust_logging_level,
491)     help='emit extra/progress information to standard error',
492)     cls=LoggingOption,
493) )
494) quiet_option = click.option(
495)     '-q',
496)     '--quiet',
497)     'logging_level',
498)     is_flag=True,
499)     flag_value=logging.ERROR,
500)     expose_value=False,
501)     callback=adjust_logging_level,
502)     help='suppress even warnings, emit only errors',
503)     cls=LoggingOption,
504) )
505) 
506) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

507) def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]:
508)     """Decorate the function with standard logging click options.
509) 
510)     Adds the three click options `-v`/`--verbose`, `-q`/`--quiet` and
511)     `--debug`, which issue callbacks to the [`log_info`][],
512)     [`silence_warnings`][] and [`log_debug`][] functions, respectively.
513) 
514)     Args:
515)         f: A callable to decorate.
516) 
517)     Returns:
518)         The decorated callable.
519) 
520)     """
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

521)     return debug_option(verbose_option(quiet_option(f)))
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

522) 
523) 
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

524) # Top-level
525) # =========
526) 
527) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

528) class _DefaultToVaultGroup(click.Group):
529)     """A helper class to implement the default-to-"vault"-subcommand behavior.
530) 
531)     Modifies internal [`click.MultiCommand`][] methods, and thus is both
532)     an implementation detail and a kludge.
533) 
534)     """
535) 
536)     def resolve_command(
537)         self, ctx: click.Context, args: list[str]
538)     ) -> tuple[str | None, click.Command | None, list[str]]:
539)         """Resolve a command, but default to "vault" instead of erroring out.
540) 
541)         Based on code from click 8.1, which appears to be essentially
542)         untouched since at least click 3.2.
543) 
544)         """
545)         cmd_name = click.utils.make_str(args[0])
546) 
547)         # ORIGINAL COMMENT
548)         # Get the command
549)         cmd = self.get_command(ctx, cmd_name)
550) 
551)         # ORIGINAL COMMENT
552)         # If we can't find the command but there is a normalization
553)         # function available, we try with that one.
554)         if (  # pragma: no cover
555)             cmd is None and ctx.token_normalize_func is not None
556)         ):
557)             cmd_name = ctx.token_normalize_func(cmd_name)
558)             cmd = self.get_command(ctx, cmd_name)
559) 
560)         # ORIGINAL COMMENT
561)         # If we don't find the command we want to show an error message
562)         # to the user that it was not provided.  However, there is
563)         # something else we should do: if the first argument looks like
564)         # an option we want to kick off parsing again for arguments to
565)         # resolve things like --help which now should go to the main
566)         # place.
567)         if cmd is None and not ctx.resilient_parsing:
568)             if click.parser.split_opt(cmd_name)[0]:
569)                 self.parse_args(ctx, ctx.args)
570)             # Instead of calling ctx.fail here, default to "vault", and
571)             # issue a deprecation warning.
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

572)             logger = logging.getLogger(PROG_NAME)
573)             deprecation = logging.getLogger(f'{PROG_NAME}.deprecation')
574)             deprecation.warning(
575)                 'A subcommand will be required in v1.0. '
576)                 'See --help for available subcommands.'
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

577)             )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

578)             logger.warning('Defaulting to subcommand "vault".')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

579)             cmd_name = 'vault'
580)             cmd = self.get_command(ctx, cmd_name)
581)             assert cmd is not None, 'Mandatory subcommand "vault" missing!'
582)             args = [cmd_name, *args]
583)         return cmd_name if cmd else None, cmd, args[1:]  # noqa: DOC201
584) 
585) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

586) class _TopLevelCLIEntryPoint(_DefaultToVaultGroup):
587)     """A minor variation of _DefaultToVaultGroup for the top-level command.
588) 
589)     When called as a function, this sets up the environment properly
590)     before invoking the actual callbacks.  Currently, this means setting
591)     up the logging subsystem and the delegation of Python warnings to
592)     the logging subsystem.
593) 
594)     The environment setup can be bypassed by calling the `.main` method
595)     directly.
596) 
597)     """
598) 
599)     def __call__(  # pragma: no cover
600)         self,
601)         *args: Any,  # noqa: ANN401
602)         **kwargs: Any,  # noqa: ANN401
603)     ) -> Any:  # noqa: ANN401
604)         """"""  # noqa: D419
605)         # Coverage testing is done with the `click.testing` module,
606)         # which does not use the `__call__` shortcut.  So it is normal
607)         # that this function is never called, and thus should be
608)         # excluded from coverage.
609)         with (
610)             StandardCLILogging.ensure_standard_logging(),
611)             StandardCLILogging.ensure_standard_warnings_logging(),
612)         ):
613)             return self.main(*args, **kwargs)
614) 
615) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

616) @click.group(
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

617)     context_settings={
618)         'help_option_names': ['-h', '--help'],
619)         'ignore_unknown_options': True,
620)         'allow_interspersed_args': False,
621)     },
622)     epilog=r"""
623)         Configuration is stored in a directory according to the
624)         DERIVEPASSPHRASE_PATH variable, which defaults to
625)         `~/.derivepassphrase` on UNIX-like systems and
626)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
Marco Ricci Fix minor typo, formatting...

Marco Ricci authored 3 months ago

627)     """,
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

628)     invoke_without_command=True,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

629)     cls=_TopLevelCLIEntryPoint,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

630) )
631) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

632) @standard_logging_options
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

633) @click.pass_context
634) def derivepassphrase(ctx: click.Context, /) -> None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

635)     """Derive a strong passphrase, deterministically, from a master secret.
636) 
637)     Using a master secret, derive a passphrase for a named service,
638)     subject to constraints e.g. on passphrase length, allowed
639)     characters, etc.  The exact derivation depends on the selected
640)     derivation scheme.  For each scheme, it is computationally
641)     infeasible to discern the master secret from the derived passphrase.
642)     The derivations are also deterministic, given the same inputs, thus
643)     the resulting passphrases need not be stored explicitly.  The
644)     service name and constraints themselves also generally need not be
645)     kept secret, depending on the scheme.
646) 
647)     The currently implemented subcommands are "vault" (for the scheme
648)     used by vault) and "export" (for exporting foreign configuration
649)     data).  See the respective `--help` output for instructions.  If no
650)     subcommand is given, we default to "vault".
651) 
652)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
653)     in v1.0, the subcommand must be specified explicitly.\f
654) 
655)     This is a [`click`][CLICK]-powered command-line interface function,
656)     and not intended for programmatic use.  Call with arguments
657)     `['--help']` to see full documentation of the interface.  (See also
658)     [`click.testing.CliRunner`][] for controlled, programmatic
659)     invocation.)
660) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

662) 
663)     """  # noqa: D301
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

664)     logger = logging.getLogger(PROG_NAME)
665)     deprecation = logging.getLogger(f'{PROG_NAME}.deprecation')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

666)     if ctx.invoked_subcommand is None:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

667)         deprecation.warning(
668)             'A subcommand will be required in v1.0. '
669)             'See --help for available subcommands.'
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

670)         )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

671)         logger.warning('Defaulting to subcommand "vault".')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

672)         # See definition of click.Group.invoke, non-chained case.
673)         with ctx:
674)             sub_ctx = derivepassphrase_vault.make_context(
675)                 'vault', ctx.args, parent=ctx
676)             )
677)             with sub_ctx:
678)                 return derivepassphrase_vault.invoke(sub_ctx)
679)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

680) 
681) 
682) # Exporter
683) # ========
684) 
685) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

686) @derivepassphrase.group(
687)     'export',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

688)     context_settings={
689)         'help_option_names': ['-h', '--help'],
690)         'ignore_unknown_options': True,
691)         'allow_interspersed_args': False,
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

692)     },
693)     invoke_without_command=True,
694)     cls=_DefaultToVaultGroup,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

695) )
696) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

697) @standard_logging_options
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

698) @click.pass_context
699) def derivepassphrase_export(ctx: click.Context, /) -> None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

700)     """Export a foreign configuration to standard output.
701) 
702)     Read a foreign system configuration, extract all information from
703)     it, and export the resulting configuration to standard output.
704) 
705)     The only available subcommand is "vault", which implements the
706)     vault-native configuration scheme.  If no subcommand is given, we
707)     default to "vault".
708) 
709)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
710)     in v1.0, the subcommand must be specified explicitly.\f
711) 
712)     This is a [`click`][CLICK]-powered command-line interface function,
713)     and not intended for programmatic use.  Call with arguments
714)     `['--help']` to see full documentation of the interface.  (See also
715)     [`click.testing.CliRunner`][] for controlled, programmatic
716)     invocation.)
717) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

719) 
720)     """  # noqa: D301
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

721)     logger = logging.getLogger(PROG_NAME)
722)     deprecation = logging.getLogger(f'{PROG_NAME}.deprecation')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

723)     if ctx.invoked_subcommand is None:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

724)         deprecation.warning(
725)             'A subcommand will be required in v1.0. '
726)             'See --help for available subcommands.'
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

727)         )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

728)         logger.warning('Defaulting to subcommand "vault".')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

729)         # See definition of click.Group.invoke, non-chained case.
730)         with ctx:
731)             sub_ctx = derivepassphrase_export_vault.make_context(
732)                 'vault', ctx.args, parent=ctx
733)             )
734)             # Constructing the subcontext above will usually already
735)             # lead to a click.UsageError, so this block typically won't
736)             # actually be called.
737)             with sub_ctx:  # pragma: no cover
738)                 return derivepassphrase_export_vault.invoke(sub_ctx)
739)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

740) 
741) 
742) def _load_data(
743)     fmt: Literal['v0.2', 'v0.3', 'storeroom'],
744)     path: str | bytes | os.PathLike[str],
745)     key: bytes,
746) ) -> Any:  # noqa: ANN401
747)     contents: bytes
748)     module: types.ModuleType
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

749)     # Use match/case here once Python 3.9 becomes unsupported.
750)     if fmt == 'v0.2':
751)         module = importlib.import_module(
752)             'derivepassphrase.exporter.vault_native'
753)         )
754)         if module.STUBBED:
755)             raise ModuleNotFoundError
756)         with open(path, 'rb') as infile:
757)             contents = base64.standard_b64decode(infile.read())
758)         return module.export_vault_native_data(
759)             contents, key, try_formats=['v0.2']
760)         )
761)     elif fmt == 'v0.3':  # noqa: RET505
762)         module = importlib.import_module(
763)             'derivepassphrase.exporter.vault_native'
764)         )
765)         if module.STUBBED:
766)             raise ModuleNotFoundError
767)         with open(path, 'rb') as infile:
768)             contents = base64.standard_b64decode(infile.read())
769)         return module.export_vault_native_data(
770)             contents, key, try_formats=['v0.3']
771)         )
772)     elif fmt == 'storeroom':
773)         module = importlib.import_module('derivepassphrase.exporter.storeroom')
774)         if module.STUBBED:
775)             raise ModuleNotFoundError
776)         return module.export_storeroom_data(path, key)
777)     else:  # pragma: no cover
778)         assert_never(fmt)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

779) 
780) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

781) @derivepassphrase_export.command(
782)     'vault',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

783)     context_settings={'help_option_names': ['-h', '--help']},
784) )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

785) @standard_logging_options
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

786) @click.option(
787)     '-f',
788)     '--format',
789)     'formats',
790)     metavar='FMT',
791)     multiple=True,
792)     default=('v0.3', 'v0.2', 'storeroom'),
793)     type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
794)     help='try the following storage formats, in order (default: v0.3, v0.2)',
795) )
796) @click.option(
797)     '-k',
798)     '--key',
799)     metavar='K',
800)     help=(
801)         'use K as the storage master key '
802)         '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or '
803)         '`USERNAME` environment variables)'
804)     ),
805) )
806) @click.argument('path', metavar='PATH', required=True)
807) @click.pass_context
808) def derivepassphrase_export_vault(
809)     ctx: click.Context,
810)     /,
811)     *,
812)     path: str | bytes | os.PathLike[str],
813)     formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
814)     key: str | bytes | None = None,
815) ) -> None:
816)     """Export a vault-native configuration to standard output.
817) 
818)     Read the vault-native configuration at PATH, extract all information
819)     from it, and export the resulting configuration to standard output.
820)     Depending on the configuration format, PATH may either be a file or
821)     a directory.  Supports the vault "v0.2", "v0.3" and "storeroom"
822)     formats.
823) 
824)     If PATH is explicitly given as `VAULT_PATH`, then use the
825)     `VAULT_PATH` environment variable to determine the correct path.
826)     (Use `./VAULT_PATH` or similar to indicate a file/directory actually
827)     named `VAULT_PATH`.)
828) 
829)     """
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

830)     logger = logging.getLogger(PROG_NAME)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

831)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
832)         path = exporter.get_vault_path()
833)     if key is None:
834)         key = exporter.get_vault_key()
835)     elif isinstance(key, str):  # pragma: no branch
836)         key = key.encode('utf-8')
837)     for fmt in formats:
838)         try:
839)             config = _load_data(fmt, path, key)
840)         except (
841)             IsADirectoryError,
842)             NotADirectoryError,
843)             ValueError,
844)             RuntimeError,
845)         ):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

846)             logger.info('Cannot load as %s: %s', fmt, path)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

847)             continue
848)         except OSError as exc:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

849)             logger.error(
850)                 'Cannot parse %r as a valid config: %s: %r',
851)                 path,
852)                 exc.strerror,
853)                 exc.filename,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

854)             )
855)             ctx.exit(1)
856)         except ModuleNotFoundError:
857)             # TODO(the-13th-letter): Use backslash continuation.
858)             # https://github.com/nedbat/coveragepy/issues/1836
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

859)             logger.error(
860)                 'Cannot load the required Python module "cryptography".'
861)             )
862)             logger.info('pip users: see the "export" extra.')
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

863)             ctx.exit(1)
864)         else:
865)             if not _types.is_vault_config(config):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

866)                 logger.error('Invalid vault config: %r', config)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

867)                 ctx.exit(1)
868)             click.echo(json.dumps(config, indent=2, sort_keys=True))
869)             break
870)     else:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

871)         logger.error('Cannot parse %r as a valid config.', path)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

872)         ctx.exit(1)
873) 
874) 
875) # Vault
876) # =====
877) 
878) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

879) def _config_filename(
Marco Ricci Make obtaining the compatib...

Marco Ricci authored 3 weeks ago

880)     subsystem: str | None = 'old settings.json',
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

881) ) -> str | bytes | pathlib.Path:
882)     """Return the filename of the configuration file for the subsystem.
883) 
884)     The (implicit default) file is currently named `settings.json`,
885)     located within the configuration directory as determined by the
886)     `DERIVEPASSPHRASE_PATH` environment variable, or by
887)     [`click.get_app_dir`][] in POSIX mode.  Depending on the requested
888)     subsystem, this will usually be a different file within that
889)     directory.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

890) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

891)     Args:
892)         subsystem:
893)             Name of the configuration subsystem whose configuration
894)             filename to return.  If not given, return the old filename
895)             from before the subcommand migration.  If `None`, return the
896)             configuration directory instead.
897) 
898)     Raises:
899)         AssertionError:
900)             An unknown subsystem was passed.
901) 
902)     Deprecated:
903)         Since v0.2.0: The implicit default subsystem and the old
904)         configuration filename are deprecated, and will be removed in v1.0.
905)         The subsystem will be mandatory to specify.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

906) 
907)     """
908)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

912)     # Use match/case here once Python 3.9 becomes unsupported.
913)     if subsystem is None:
914)         return path
Marco Ricci Make obtaining the compatib...

Marco Ricci authored 3 weeks ago

915)     elif subsystem == 'vault':  # noqa: RET505
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

916)         filename = f'{subsystem}.json'
Marco Ricci Make obtaining the compatib...

Marco Ricci authored 3 weeks ago

917)     elif subsystem == 'old settings.json':
918)         filename = 'settings.json'
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

919)     else:  # pragma: no cover
920)         msg = f'Unknown configuration subsystem: {subsystem!r}'
921)         raise AssertionError(msg)
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

923) 
924) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

930) 
931)     Returns:
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

933) 
934)     Raises:
935)         OSError:
936)             There was an OS error accessing the file.
937)         ValueError:
938)             The data loaded from the file is not a vault(1)-compatible
939)             config.
940) 
941)     """
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

947)     return data
948) 
949) 
Marco Ricci Permit one flaky test and f...

Marco Ricci authored 3 months ago

950) def _migrate_and_load_old_config() -> tuple[
951)     _types.VaultConfig, OSError | None
952) ]:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

958) 
959)     Returns:
960)         The vault settings, and an optional exception encountered during
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

963) 
964)     Raises:
965)         OSError:
966)             There was an OS error accessing the old file.
967)         ValueError:
968)             The data loaded from the file is not a vault(1)-compatible
969)             config.
970) 
971)     """
972)     new_filename = _config_filename(subsystem='vault')
Marco Ricci Make obtaining the compatib...

Marco Ricci authored 3 weeks ago

973)     old_filename = _config_filename(subsystem='old settings.json')
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

974)     with open(old_filename, 'rb') as fileobj:
975)         data = json.load(fileobj)
976)     if not _types.is_vault_config(data):
977)         raise ValueError(_INVALID_VAULT_CONFIG)
978)     try:
979)         os.replace(old_filename, new_filename)
980)     except OSError as exc:
981)         return data, exc
982)     else:
983)         return data, None
984) 
985) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

988) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

991) 
992)     Args:
993)         config:
994)             vault configuration to save.
995) 
996)     Raises:
997)         OSError:
998)             There was an OS error accessing or writing the file.
999)         ValueError:
1000)             The data cannot be stored as a vault(1)-compatible config.
1001) 
1002)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1006)     filedir = os.path.dirname(os.path.abspath(filename))
1007)     try:
1008)         os.makedirs(filedir, exist_ok=False)
1009)     except FileExistsError:
1010)         if not os.path.isdir(filedir):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1013)         json.dump(config, fileobj)
1014) 
1015) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1023) 
1024)     Args:
1025)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

1028) 
1029)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1032) 
1033)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1034)         KeyError:
1035)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
1036)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1051) 
1052)     """
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

1053)     with ssh_agent.SSHAgentClient.ensure_agent_subcontext(conn) as client:
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1057)             raise RuntimeError(_AGENT_COMMUNICATION_ERROR) from e
Marco Ricci Publish polished `is_suitab...

Marco Ricci authored 1 month ago

1058)         suitable_keys = copy.copy(all_key_comment_pairs)
1059)         for pair in all_key_comment_pairs:
1060)             key, _comment = pair
1061)             if vault.Vault.is_suitable_ssh_key(key, client=client):
1062)                 yield pair
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1063)     if not suitable_keys:  # pragma: no cover
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1065) 
1066) 
1067) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1070)     single_choice_prompt: str = 'Confirm this choice?',
1071) ) -> int:
1072)     """Prompt user for a choice among the given items.
1073) 
1074)     Print the heading, if any, then present the items to the user.  If
1075)     there are multiple items, prompt the user for a selection, validate
1076)     the choice, then return the list index of the selected item.  If
1077)     there is only a single item, request confirmation for that item
1078)     instead, and return the correct index.
1079) 
1080)     Args:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1083)         heading:
1084)             A heading for the list of items, to print immediately
1085)             before.  Defaults to a reasonable standard heading.  If
1086)             explicitly empty, print no heading.
1087)         single_choice_prompt:
1088)             The confirmation prompt if there is only a single possible
1089)             choice.  Defaults to a reasonable standard prompt.
1090) 
1091)     Returns:
1092)         An index into the items sequence, indicating the user's
1093)         selection.
1094) 
1095)     Raises:
1096)         IndexError:
1097)             The user made an invalid or empty selection, or requested an
1098)             abort.
1099) 
1100)     """
1101)     n = len(items)
1102)     if heading:
1103)         click.echo(click.style(heading, bold=True))
1104)     for i, x in enumerate(items, start=1):
1105)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
1106)         click.echo(' ', nl=False)
1107)         click.echo(x)
1108)     if n > 1:
1109)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
1110)         choice = click.prompt(
1111)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1112)             err=True,
1113)             type=choices,
1114)             show_choices=False,
1115)             show_default=False,
1116)             default='',
1117)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1121)     prompt_suffix = (
1122)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
1123)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1124)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1125)         click.confirm(
1126)             single_choice_prompt,
1127)             prompt_suffix=prompt_suffix,
1128)             err=True,
1129)             abort=True,
1130)             default=False,
1131)             show_default=False,
1132)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1133)     except click.Abort:
1134)         raise IndexError(_EMPTY_SELECTION) from None
1135)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1136) 
1137) 
1138) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1146) 
1147)     Args:
1148)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

1151) 
1152)     Returns:
1153)         The selected SSH key.
1154) 
1155)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1156)         KeyError:
1157)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
1158)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1166)         IndexError:
1167)             The user made an invalid or empty selection, or requested an
1168)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1176)     """
1177)     suitable_keys = list(_get_suitable_ssh_keys(conn))
1178)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

1183)         remaining_key_display_length = KEY_DISPLAY_LENGTH - 1 - len(keytype)
1184)         key_extract = min(
1185)             key_str,
1186)             '...' + key_str[-remaining_key_display_length:],
1187)             key=len,
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1192)         key_listing,
1193)         heading='Suitable SSH keys:',
1194)         single_choice_prompt='Use this key?',
1195)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1196)     return suitable_keys[choice].key
1197) 
1198) 
1199) def _prompt_for_passphrase() -> str:
1200)     """Interactively prompt for the passphrase.
1201) 
1202)     Calls [`click.prompt`][] internally.  Moved into a separate function
1203)     mainly for testing/mocking purposes.
1204) 
1205)     Returns:
1206)         The user input.
1207) 
1208)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

1209)     return cast(
1210)         str,
1211)         click.prompt(
1212)             'Passphrase',
1213)             default='',
1214)             hide_input=True,
1215)             show_default=False,
1216)             err=True,
1217)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1219) 
1220) 
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1221) class _ORIGIN(enum.Enum):
1222)     INTERACTIVE: str = 'interactive'
1223) 
1224) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1227)     value: dict[str, Any],
1228)     *,
1229)     form: Literal['NFC', 'NFD', 'NFKC', 'NFKD'] = 'NFC',
1230) ) -> None:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1231)     logger = logging.getLogger(PROG_NAME)
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1232)     if 'phrase' in value:
1233)         phrase = value['phrase']
1234)         if not unicodedata.is_normalized(form, phrase):
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1235)             formatted_key = (
1236)                 key.value
1237)                 if isinstance(key, _ORIGIN)
1238)                 else _types.json_path(key)
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1239)             )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1240)             logger.warning(
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1241)                 (
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1242)                     'the %s passphrase is not %s-normalized. '
1243)                     'Make sure to double-check this is really the '
1244)                     'passphrase you want.'
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1245)                 ),
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1246)                 formatted_key,
1247)                 form,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1248)             )
1249) 
1250) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1261) 
1262) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1270) 
1271) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1278)     """
1279) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1280) 
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1281) class CompatibilityOption(OptionGroupOption):
1282)     """Compatibility and incompatibility options for the CLI."""
1283) 
1284)     option_group_name = 'Options concerning compatibility with other tools'
1285) 
1286) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1287) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1292)     """Check that the occurrence constraint is valid (int, 0 or larger).
1293) 
1294)     Args:
1295)         ctx: The `click` context.
1296)         param: The current command-line parameter.
1297)         value: The parameter value to be checked.
1298) 
1299)     Returns:
1300)         The parsed parameter value.
1301) 
1302)     Raises:
1303)         click.BadParameter: The parameter value is invalid.
1304) 
1305)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1308)     if value is None:
1309)         return value
1310)     if isinstance(value, int):
1311)         int_value = value
1312)     else:
1313)         try:
1314)             int_value = int(value, 10)
1315)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1321)     return int_value
1322) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1323) 
1324) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1329)     """Check that the length is valid (int, 1 or larger).
1330) 
1331)     Args:
1332)         ctx: The `click` context.
1333)         param: The current command-line parameter.
1334)         value: The parameter value to be checked.
1335) 
1336)     Returns:
1337)         The parsed parameter value.
1338) 
1339)     Raises:
1340)         click.BadParameter: The parameter value is invalid.
1341) 
1342)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1345)     if value is None:
1346)         return value
1347)     if isinstance(value, int):
1348)         int_value = value
1349)     else:
1350)         try:
1351)             int_value = int(value, 10)
1352)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1358)     return int_value
1359) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1360) 
1361) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1362) # Enter notes below the line with the cut mark (ASCII scissors and
1363) # dashes).  Lines above the cut mark (such as this one) will be ignored.
1364) #
1365) # If you wish to clear the notes, leave everything beyond the cut mark
1366) # blank.  However, if you leave the *entire* file blank, also removing
1367) # the cut mark, then the edit is aborted, and the old notes contents are
1368) # retained.
1369) #
1370) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1372) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
1373) 
1374) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

1375) @derivepassphrase.command(
1376)     'vault',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1388)     """,
1389) )
1390) @click.option(
1391)     '-p',
1392)     '--phrase',
1393)     'use_phrase',
1394)     is_flag=True,
1395)     help='prompts you for your passphrase',
1396)     cls=PasswordGenerationOption,
1397) )
1398) @click.option(
1399)     '-k',
1400)     '--key',
1401)     'use_key',
1402)     is_flag=True,
1403)     help='uses your SSH private key to generate passwords',
1404)     cls=PasswordGenerationOption,
1405) )
1406) @click.option(
1407)     '-l',
1408)     '--length',
1409)     metavar='NUMBER',
1410)     callback=_validate_length,
1411)     help='emits password of length NUMBER',
1412)     cls=PasswordGenerationOption,
1413) )
1414) @click.option(
1415)     '-r',
1416)     '--repeat',
1417)     metavar='NUMBER',
1418)     callback=_validate_occurrence_constraint,
1419)     help='allows maximum of NUMBER repeated adjacent chars',
1420)     cls=PasswordGenerationOption,
1421) )
1422) @click.option(
1423)     '--lower',
1424)     metavar='NUMBER',
1425)     callback=_validate_occurrence_constraint,
1426)     help='includes at least NUMBER lowercase letters',
1427)     cls=PasswordGenerationOption,
1428) )
1429) @click.option(
1430)     '--upper',
1431)     metavar='NUMBER',
1432)     callback=_validate_occurrence_constraint,
1433)     help='includes at least NUMBER uppercase letters',
1434)     cls=PasswordGenerationOption,
1435) )
1436) @click.option(
1437)     '--number',
1438)     metavar='NUMBER',
1439)     callback=_validate_occurrence_constraint,
1440)     help='includes at least NUMBER digits',
1441)     cls=PasswordGenerationOption,
1442) )
1443) @click.option(
1444)     '--space',
1445)     metavar='NUMBER',
1446)     callback=_validate_occurrence_constraint,
1447)     help='includes at least NUMBER spaces',
1448)     cls=PasswordGenerationOption,
1449) )
1450) @click.option(
1451)     '--dash',
1452)     metavar='NUMBER',
1453)     callback=_validate_occurrence_constraint,
1454)     help='includes at least NUMBER "-" or "_"',
1455)     cls=PasswordGenerationOption,
1456) )
1457) @click.option(
1458)     '--symbol',
1459)     metavar='NUMBER',
1460)     callback=_validate_occurrence_constraint,
1461)     help='includes at least NUMBER symbol chars',
1462)     cls=PasswordGenerationOption,
1463) )
1464) @click.option(
1465)     '-n',
1466)     '--notes',
1467)     'edit_notes',
1468)     is_flag=True,
1469)     help='spawn an editor to edit notes for SERVICE',
1470)     cls=ConfigurationOption,
1471) )
1472) @click.option(
1473)     '-c',
1474)     '--config',
1475)     'store_config_only',
1476)     is_flag=True,
1477)     help='saves the given settings for SERVICE or global',
1478)     cls=ConfigurationOption,
1479) )
1480) @click.option(
1481)     '-x',
1482)     '--delete',
1483)     'delete_service_settings',
1484)     is_flag=True,
1485)     help='deletes settings for SERVICE',
1486)     cls=ConfigurationOption,
1487) )
1488) @click.option(
1489)     '--delete-globals',
1490)     is_flag=True,
1491)     help='deletes the global shared settings',
1492)     cls=ConfigurationOption,
1493) )
1494) @click.option(
1495)     '-X',
1496)     '--clear',
1497)     'clear_all_settings',
1498)     is_flag=True,
1499)     help='deletes all settings',
1500)     cls=ConfigurationOption,
1501) )
1502) @click.option(
1503)     '-e',
1504)     '--export',
1505)     'export_settings',
1506)     metavar='PATH',
1507)     help='export all saved settings into file PATH',
1508)     cls=StorageManagementOption,
1509) )
1510) @click.option(
1511)     '-i',
1512)     '--import',
1513)     'import_settings',
1514)     metavar='PATH',
1515)     help='import saved settings from file PATH',
1516)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1517) )
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1518) @click.option(
1519)     '--overwrite-existing/--merge-existing',
1520)     'overwrite_config',
1521)     default=False,
1522)     help='overwrite or merge (default) the existing configuration',
1523)     cls=CompatibilityOption,
1524) )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1525) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1526) @standard_logging_options
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1530)     ctx: click.Context,
1531)     /,
1532)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1533)     service: str | None = None,
1534)     use_phrase: bool = False,
1535)     use_key: bool = False,
1536)     length: int | None = None,
1537)     repeat: int | None = None,
1538)     lower: int | None = None,
1539)     upper: int | None = None,
1540)     number: int | None = None,
1541)     space: int | None = None,
1542)     dash: int | None = None,
1543)     symbol: int | None = None,
1544)     edit_notes: bool = False,
1545)     store_config_only: bool = False,
1546)     delete_service_settings: bool = False,
1547)     delete_globals: bool = False,
1548)     clear_all_settings: bool = False,
1549)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
1550)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1551)     overwrite_config: bool = False,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1555)     Using a master passphrase or a master SSH key, derive a passphrase
1556)     for SERVICE, subject to length, character and character repetition
1557)     constraints.  The derivation is cryptographically strong, meaning
1558)     that even if a single passphrase is compromised, guessing the master
1559)     passphrase or a different service's passphrase is computationally
1560)     infeasible.  The derivation is also deterministic, given the same
1561)     inputs, thus the resulting passphrase need not be stored explicitly.
1562)     The service name and constraints themselves also need not be kept
1563)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1575) 
1576)     Parameters:
1577)         ctx (click.Context):
1578)             The `click` context.
1579) 
1580)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1633)         export_settings:
1634)             Command-line argument `-e`/`--export`.  If a file object,
1635)             then it must be open for writing and accept `str` inputs.
1636)             Otherwise, a filename to open for writing.  Using `-` for
1637)             standard output is supported.
1638)         import_settings:
1639)             Command-line argument `-i`/`--import`.  If a file object, it
1640)             must be open for reading and yield `str` values.  Otherwise,
1641)             a filename to open for reading.  Using `-` for standard
1642)             input is supported.
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1643)         overwrite_config:
1644)             Command-line arguments `--overwrite-existing` (True) and
1645)             `--merge-existing` (False).  Controls whether config saving
1646)             and config importing overwrite existing configurations, or
1647)             merge them section-wise instead.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1649)     """  # noqa: D301
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1650)     logger = logging.getLogger(PROG_NAME)
1651)     deprecation = logging.getLogger(PROG_NAME + '.deprecation')
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

1657)             # Use match/case here once Python 3.9 becomes unsupported.
1658)             if isinstance(param, PasswordGenerationOption):
1659)                 group = PasswordGenerationOption
1660)             elif isinstance(param, ConfigurationOption):
1661)                 group = ConfigurationOption
1662)             elif isinstance(param, StorageManagementOption):
1663)                 group = StorageManagementOption
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1664)             elif isinstance(param, LoggingOption):
1665)                 group = LoggingOption
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1666)             elif isinstance(param, CompatibilityOption):
1667)                 group = CompatibilityOption
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

1668)             elif isinstance(param, OptionGroupOption):
1669)                 raise AssertionError(  # noqa: DOC501,TRY003,TRY004
1670)                     f'Unknown option group for {param!r}'  # noqa: EM102
1671)                 )
1672)             else:
1673)                 group = click.Option
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1674)             options_in_group.setdefault(group, []).append(param)
1675)         params_by_str[param.human_readable_name] = param
1676)         for name in param.opts + param.secondary_opts:
1677)             params_by_str[name] = param
1678) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1682)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1686)         if isinstance(param, str):
1687)             param = params_by_str[param]
1688)         assert isinstance(param, click.Parameter)
1689)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1690)             return
1691)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1694)             assert isinstance(other, click.Parameter)
1695)             if other != param and is_param_set(other):
1696)                 opt_str = param.opts[0]
1697)                 other_str = other.opts[0]
1698)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1701) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1702)     def err(msg: Any, *args: Any, **kwargs: Any) -> NoReturn:  # noqa: ANN401
1703)         stacklevel = kwargs.pop('stacklevel', 1)
1704)         stacklevel += 1
1705)         logger.error(msg, *args, stacklevel=stacklevel, **kwargs)
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1706)         ctx.exit(1)
1707) 
Marco Ricci Fix user interface errors i...

Marco Ricci authored 3 weeks ago

1708)     def key_to_phrase(key_: str | bytes | bytearray, /) -> bytes | bytearray:
1709)         key = base64.standard_b64decode(key_)
1710)         try:
1711)             with ssh_agent.SSHAgentClient.ensure_agent_subcontext() as client:
1712)                 try:
1713)                     return vault.Vault.phrase_from_key(key, conn=client)
1714)                 except ssh_agent.SSHAgentFailedError as e:
1715)                     try:
1716)                         keylist = client.list_keys()
1717)                     except ssh_agent.SSHAgentFailedError:
1718)                         pass
1719)                     except Exception as e2:  # noqa: BLE001
1720)                         e.__context__ = e2
1721)                     else:
1722)                         if not any(k == key for k, _ in keylist):
1723)                             err(
1724)                                 'The requested SSH key is not loaded '
1725)                                 'into the agent.'
1726)                             )
1727)                     err(e)
1728)         except KeyError:
1729)             err('Cannot find running SSH agent; check SSH_AUTH_SOCK')
1730)         except NotImplementedError:
1731)             err(
1732)                 'Cannot connect to SSH agent because '
1733)                 'this Python version does not support UNIX domain sockets'
1734)             )
1735)         except OSError as e:
1736)             err('Cannot connect to SSH agent: %s', e.strerror)
1737) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1739)         try:
1740)             return _load_config()
1741)         except FileNotFoundError:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1742)             try:
1743)                 backup_config, exc = _migrate_and_load_old_config()
1744)             except FileNotFoundError:
1745)                 return {'services': {}}
Marco Ricci Make obtaining the compatib...

Marco Ricci authored 3 weeks ago

1746)             old_name = os.path.basename(
1747)                 _config_filename(subsystem='old settings.json')
1748)             )
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1749)             new_name = os.path.basename(_config_filename(subsystem='vault'))
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1750)             deprecation.warning(
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1751)                 (
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1752)                     'Using deprecated v0.1-style config file %r, '
1753)                     'instead of v0.2-style %r.  '
1754)                     'Support for v0.1-style config filenames will be '
1755)                     'removed in v1.0.'
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1756)                 ),
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1757)                 old_name,
1758)                 new_name,
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1759)             )
1760)             if isinstance(exc, OSError):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1761)                 logger.warning(
1762)                     'Failed to migrate to %r: %s: %r',
1763)                     new_name,
1764)                     exc.strerror,
1765)                     exc.filename,
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1766)                 )
1767)             else:
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

1768)                 deprecation.info('Successfully migrated to %r.', new_name)
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1769)             return backup_config
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1770)         except OSError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1771)             err('Cannot load config: %s: %r', e.strerror, e.filename)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1772)         except Exception as e:  # noqa: BLE001
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1773)             err('Cannot load config: %s', str(e), exc_info=e)
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1774) 
1775)     def put_config(config: _types.VaultConfig, /) -> None:
1776)         try:
1777)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1778)         except OSError as exc:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1779)             err('Cannot store config: %s: %r', exc.strerror, exc.filename)
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1780)         except Exception as exc:  # noqa: BLE001
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1781)             err('Cannot store config: %s', str(exc), exc_info=exc)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1784) 
1785)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1790)                     opt, *options_in_group[PasswordGenerationOption]
1791)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1796)                 opt,
1797)                 *options_in_group[ConfigurationOption],
1798)                 *options_in_group[StorageManagementOption],
1799)             )
Marco Ricci Correctly model vault globa...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 2 months ago

1807)             raise click.UsageError(msg)  # noqa: DOC501
1808)     sv_options = [params_by_str['--notes'], params_by_str['--delete']]
1809)     for param in sv_options:
1810)         if is_param_set(param) and not service:
1811)             opt_str = param.opts[0]
1812)             msg = f'{opt_str} requires a SERVICE'
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

1814)     no_sv_options = [
1815)         params_by_str['--delete-globals'],
1816)         params_by_str['--clear'],
1817)         *options_in_group[StorageManagementOption],
1818)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

1825)     if service == '':  # noqa: PLC1901
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1826)         logger.warning(
1827)             'An empty SERVICE is not supported by vault(1).  '
1828)             'For compatibility, this will be treated as if SERVICE '
1829)             'was not supplied, i.e., it will error out, or '
1830)             'operate on global settings.'
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

1831)         )
1832) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1833)     if edit_notes:
1834)         assert service is not None
1835)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1842)             while notes_lines:
1843)                 line = notes_lines.popleft()
1844)                 if line.startswith(DEFAULT_NOTES_MARKER):
1845)                     notes_value = ''.join(notes_lines)
1846)                     break
1847)             else:
1848)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1851)                 notes_value.strip('\n')
1852)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1854)     elif delete_service_settings:
1855)         assert service is not None
1856)         configuration = get_config()
1857)         if service in configuration['services']:
1858)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1860)     elif delete_globals:
1861)         configuration = get_config()
1862)         if 'global' in configuration:
1863)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1867)     elif import_settings:
1868)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1871)             infile = (
1872)                 cast(TextIO, import_settings)
1873)                 if hasattr(import_settings, 'close')
1874)                 else click.open_file(os.fspath(import_settings), 'rt')
1875)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1876)             with infile:
1877)                 maybe_config = json.load(infile)
1878)         except json.JSONDecodeError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1879)             err('Cannot load config: cannot decode JSON: %s', e)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1880)         except OSError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1881)             err('Cannot load config: %s: %r', e.strerror, e.filename)
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1882)         cleaned = _types.clean_up_falsy_vault_config_values(maybe_config)
1883)         if not _types.is_vault_config(maybe_config):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1884)             err('Cannot load config: %s', _INVALID_VAULT_CONFIG)
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1885)         assert cleaned is not None
1886)         for step in cleaned:
1887)             # These are never fatal errors, because the semantics of
1888)             # vault upon encountering these settings are ill-specified,
1889)             # but not ill-defined.
1890)             if step.action == 'replace':
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1891)                 logger.warning(
1892)                     'Replacing invalid value %s for key %s with %s.',
1893)                     json.dumps(step.old_value),
1894)                     _types.json_path(step.path),
1895)                     json.dumps(step.new_value),
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1896)                 )
1897)             else:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1898)                 logger.warning(
1899)                     'Removing ineffective setting %s = %s.',
1900)                     _types.json_path(step.path),
1901)                     json.dumps(step.old_value),
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

1903)         if '' in maybe_config['services']:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1904)             logger.warning(
1905)                 (
1906)                     'An empty SERVICE is not supported by vault(1), '
1907)                     'and the empty-string service settings will be '
1908)                     'inaccessible and ineffective.  To ensure that '
1909)                     'vault(1) and %s see the settings, move them '
1910)                     'into the "global" section.'
1911)                 ),
1912)                 PROG_NAME,
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

1913)             )
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1914)         form = cast(
1915)             Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1916)             maybe_config.get('global', {}).get(
1917)                 'unicode_normalization_form', 'NFC'
1918)             ),
1919)         )
1920)         assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1921)         _check_for_misleading_passphrase(
1922)             ('global',),
1923)             cast(dict[str, Any], maybe_config.get('global', {})),
1924)             form=form,
1925)         )
1926)         for key, value in maybe_config['services'].items():
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1930)                 form=form,
1931)             )
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1932)         if overwrite_config:
1933)             put_config(maybe_config)
1934)         else:
1935)             configuration = get_config()
1936)             merged_config: collections.ChainMap[str, Any] = (
1937)                 collections.ChainMap(
1938)                     {
1939)                         'services': collections.ChainMap(
1940)                             maybe_config['services'],
1941)                             configuration['services'],
1942)                         ),
1943)                     },
1944)                     {'global': maybe_config['global']}
1945)                     if 'global' in maybe_config
1946)                     else {},
1947)                     {'global': configuration['global']}
1948)                     if 'global' in configuration
1949)                     else {},
1950)                 )
1951)             )
1952)             new_config: Any = {
1953)                 k: dict(v) if isinstance(v, collections.ChainMap) else v
1954)                 for k, v in sorted(merged_config.items())
1955)             }
1956)             assert _types.is_vault_config(new_config)
1957)             put_config(new_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1958)     elif export_settings:
1959)         configuration = get_config()
1960)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1963)             outfile = (
1964)                 cast(TextIO, export_settings)
1965)                 if hasattr(export_settings, 'close')
1966)                 else click.open_file(os.fspath(export_settings), 'wt')
1967)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1968)             with outfile:
1969)                 json.dump(configuration, outfile)
1970)         except OSError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1971)             err('Cannot store config: %s: %r', e.strerror, e.filename)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1979)         service_keys = {
1980)             'key',
1981)             'phrase',
1982)             'length',
1983)             'repeat',
1984)             'lower',
1985)             'upper',
1986)             'number',
1987)             'space',
1988)             'dash',
1989)             'symbol',
1990)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1992)             {
1993)                 k: v
1994)                 for k, v in locals().items()
1995)                 if k in service_keys and v is not None
1996)             },
1997)             cast(
1998)                 dict[str, Any],
1999)                 configuration['services'].get(service or '', {}),
2000)             ),
2001)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2002)         )
2003)         if use_key:
2004)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

2005)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
2006)                     'ASCII'
2007)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

2012)             except NotImplementedError:
2013)                 err(
2014)                     'Cannot connect to SSH agent because '
2015)                     'this Python version does not support UNIX domain sockets'
2016)                 )
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

2017)             except OSError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

2018)                 err('Cannot connect to SSH agent: %s', e.strerror)
Marco Ricci Add a specific error class...

Marco Ricci authored 4 months ago

2019)             except (
2020)                 LookupError,
2021)                 RuntimeError,
2022)                 ssh_agent.SSHAgentFailedError,
2023)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2025)         elif use_phrase:
2026)             maybe_phrase = _prompt_for_passphrase()
2027)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2029)             else:
2030)                 phrase = maybe_phrase
2031)         if store_config_only:
2032)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

2033)             view = (
2034)                 collections.ChainMap(*settings.maps[:2])
2035)                 if service
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

2038)             if use_key:
2039)                 view['key'] = key
2040)             elif use_phrase:
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

2043)                 _check_for_misleading_passphrase(
2044)                     ('services', service) if service else ('global',),
2045)                     {'phrase': phrase},
2046)                 )
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

2047)                 if 'key' in settings:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

2048)                     logger.warning(
2049)                         (
2050)                             'Setting a %s passphrase is ineffective '
2051)                             'because a key is also set.'
2052)                         ),
2053)                         settings_type,
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

2059)                     f'actual settings'
2060)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

2061)                 raise click.UsageError(msg)
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

2062)             subtree: dict[str, Any] = (
2063)                 configuration['services'].setdefault(service, {})  # type: ignore[assignment]
2064)                 if service
2065)                 else configuration.setdefault('global', {})
2066)             )
2067)             if overwrite_config:
2068)                 subtree.clear()
2069)             subtree.update(view)
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2074)         else:
2075)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

2078)             kwargs: dict[str, Any] = {
2079)                 k: v
2080)                 for k, v in settings.items()
2081)                 if k in service_keys and v is not None
2082)             }
2083) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

2084)             if use_phrase:
2085)                 form = cast(
2086)                     Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
2087)                     configuration.get('global', {}).get(
2088)                         'unicode_normalization_form', 'NFC'
2089)                     ),
2090)                 )
2091)                 assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
2092)                 _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

2094)                 )
2095) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

2106)             elif kwargs.get('phrase'):
2107)                 pass
2108)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

2111)                     'or in configuration'
2112)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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