f36630533e3f8d9932e4b1b2975ec3d8bde7b45a
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 Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

15) import functools
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

20) import os
Marco Ricci Support exporting the `vaul...

Marco Ricci authored 2 weeks ago

21) import shlex
Marco Ricci Introduce a central user co...

Marco Ricci authored 3 weeks ago

22) import sys
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

34) 
35) import click
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

38)     ParamSpec,
39)     Self,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

40)     assert_never,
41) )
42) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

45) 
Marco Ricci Introduce a central user co...

Marco Ricci authored 3 weeks ago

46) if sys.version_info >= (3, 11):
47)     import tomllib
48) else:
49)     import tomli as tomllib
50) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

51) if TYPE_CHECKING:
52)     import pathlib
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 5 months ago

58)         Sequence,
59)     )
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

60) 
61) __author__ = dpp.__author__
62) __version__ = dpp.__version__
63) 
64) __all__ = ('derivepassphrase',)
65) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 5 months ago

68) 
69) # Error messages
70) _INVALID_VAULT_CONFIG = 'Invalid vault config'
71) _AGENT_COMMUNICATION_ERROR = 'Error communicating with the SSH agent'
72) _NO_USABLE_KEYS = 'No usable SSH keys were found'
73) _EMPTY_SELECTION = 'Empty selection'
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

74) 
75) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

355)     ctx: click.Context,
356)     /,
357)     param: click.Parameter | None = None,
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

365) 
366)     """
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

367)     # Note: If multiple options use this callback, then we will be
368)     # called multiple times.  Ensure the runs are idempotent.
369)     if param is None or value is None or ctx.resilient_parsing:
370)         return
371)     StandardCLILogging.cli_handler.setLevel(value)
372)     logging.getLogger(StandardCLILogging.package_name).setLevel(value)
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

373) 
374) 
Marco Ricci Shift option parsing and gr...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

474) class LoggingOption(OptionGroupOption):
475)     """Logging options for the CLI."""
476) 
477)     option_group_name = 'Logging'
478)     epilog = ''
479) 
480) 
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

515) def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]:
516)     """Decorate the function with standard logging click options.
517) 
518)     Adds the three click options `-v`/`--verbose`, `-q`/`--quiet` and
519)     `--debug`, which issue callbacks to the [`log_info`][],
520)     [`silence_warnings`][] and [`log_debug`][] functions, respectively.
521) 
522)     Args:
523)         f: A callable to decorate.
524) 
525)     Returns:
526)         The decorated callable.
527) 
528)     """
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

530) 
531) 
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

532) # Top-level
533) # =========
534) 
535) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

580)             logger = logging.getLogger(PROG_NAME)
581)             deprecation = logging.getLogger(f'{PROG_NAME}.deprecation')
582)             deprecation.warning(
583)                 'A subcommand will be required in v1.0. '
584)                 'See --help for available subcommands.'
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

587)             cmd_name = 'vault'
588)             cmd = self.get_command(ctx, cmd_name)
589)             assert cmd is not None, 'Mandatory subcommand "vault" missing!'
590)             args = [cmd_name, *args]
591)         return cmd_name if cmd else None, cmd, args[1:]  # noqa: DOC201
592) 
593) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

625)     context_settings={
626)         'help_option_names': ['-h', '--help'],
627)         'ignore_unknown_options': True,
628)         'allow_interspersed_args': False,
629)     },
630)     epilog=r"""
631)         Configuration is stored in a directory according to the
632)         DERIVEPASSPHRASE_PATH variable, which defaults to
633)         `~/.derivepassphrase` on UNIX-like systems and
634)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
Marco Ricci Fix minor typo, formatting...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

680)         # See definition of click.Group.invoke, non-chained case.
681)         with ctx:
682)             sub_ctx = derivepassphrase_vault.make_context(
683)                 'vault', ctx.args, parent=ctx
684)             )
685)             with sub_ctx:
686)                 return derivepassphrase_vault.invoke(sub_ctx)
687)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

688) 
689) 
690) # Exporter
691) # ========
692) 
693) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

694) @derivepassphrase.group(
695)     'export',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

696)     context_settings={
697)         'help_option_names': ['-h', '--help'],
698)         'ignore_unknown_options': True,
699)         'allow_interspersed_args': False,
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

700)     },
701)     invoke_without_command=True,
702)     cls=_DefaultToVaultGroup,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

737)         # See definition of click.Group.invoke, non-chained case.
738)         with ctx:
739)             sub_ctx = derivepassphrase_export_vault.make_context(
740)                 'vault', ctx.args, parent=ctx
741)             )
742)             # Constructing the subcontext above will usually already
743)             # lead to a click.UsageError, so this block typically won't
744)             # actually be called.
745)             with sub_ctx:  # pragma: no cover
746)                 return derivepassphrase_export_vault.invoke(sub_ctx)
747)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

748) 
749) 
750) def _load_data(
751)     fmt: Literal['v0.2', 'v0.3', 'storeroom'],
752)     path: str | bytes | os.PathLike[str],
753)     key: bytes,
754) ) -> Any:  # noqa: ANN401
755)     contents: bytes
756)     module: types.ModuleType
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

787) 
788) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

789) @derivepassphrase_export.command(
790)     'vault',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

839)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
840)         path = exporter.get_vault_path()
841)     if key is None:
842)         key = exporter.get_vault_key()
843)     elif isinstance(key, str):  # pragma: no branch
844)         key = key.encode('utf-8')
845)     for fmt in formats:
846)         try:
847)             config = _load_data(fmt, path, key)
848)         except (
849)             IsADirectoryError,
850)             NotADirectoryError,
851)             ValueError,
852)             RuntimeError,
853)         ):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

857)             logger.error(
858)                 'Cannot parse %r as a valid config: %s: %r',
859)                 path,
860)                 exc.strerror,
861)                 exc.filename,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

862)             )
863)             ctx.exit(1)
864)         except ModuleNotFoundError:
865)             # TODO(the-13th-letter): Use backslash continuation.
866)             # https://github.com/nedbat/coveragepy/issues/1836
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

867)             logger.error(
868)                 'Cannot load the required Python module "cryptography".'
869)             )
870)             logger.info('pip users: see the "export" extra.')
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

871)             ctx.exit(1)
872)         else:
873)             if not _types.is_vault_config(config):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

875)                 ctx.exit(1)
876)             click.echo(json.dumps(config, indent=2, sort_keys=True))
877)             break
878)     else:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

880)         ctx.exit(1)
881) 
882) 
883) # Vault
884) # =====
885) 
886) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

889) ) -> str | bytes | pathlib.Path:
890)     """Return the filename of the configuration file for the subsystem.
891) 
892)     The (implicit default) file is currently named `settings.json`,
893)     located within the configuration directory as determined by the
894)     `DERIVEPASSPHRASE_PATH` environment variable, or by
895)     [`click.get_app_dir`][] in POSIX mode.  Depending on the requested
896)     subsystem, this will usually be a different file within that
897)     directory.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

898) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

899)     Args:
900)         subsystem:
901)             Name of the configuration subsystem whose configuration
902)             filename to return.  If not given, return the old filename
903)             from before the subcommand migration.  If `None`, return the
904)             configuration directory instead.
905) 
906)     Raises:
907)         AssertionError:
908)             An unknown subsystem was passed.
909) 
910)     Deprecated:
911)         Since v0.2.0: The implicit default subsystem and the old
912)         configuration filename are deprecated, and will be removed in v1.0.
913)         The subsystem will be mandatory to specify.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

914) 
915)     """
916)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

924)         filename = f'{subsystem}.json'
Marco Ricci Introduce a central user co...

Marco Ricci authored 3 weeks ago

925)     elif subsystem == 'user configuration':
926)         filename = 'config.toml'
Marco Ricci Make obtaining the compatib...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

933) 
934) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

940) 
941)     Returns:
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

943) 
944)     Raises:
945)         OSError:
946)             There was an OS error accessing the file.
947)         ValueError:
948)             The data loaded from the file is not a vault(1)-compatible
949)             config.
950) 
951)     """
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

957)     return data
958) 
959) 
Marco Ricci Permit one flaky test and f...

Marco Ricci authored 2 months ago

960) def _migrate_and_load_old_config() -> tuple[
961)     _types.VaultConfig, OSError | None
962) ]:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

968) 
969)     Returns:
970)         The vault settings, and an optional exception encountered during
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

973) 
974)     Raises:
975)         OSError:
976)             There was an OS error accessing the old file.
977)         ValueError:
978)             The data loaded from the file is not a vault(1)-compatible
979)             config.
980) 
981)     """
982)     new_filename = _config_filename(subsystem='vault')
Marco Ricci Make obtaining the compatib...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

984)     with open(old_filename, 'rb') as fileobj:
985)         data = json.load(fileobj)
986)     if not _types.is_vault_config(data):
987)         raise ValueError(_INVALID_VAULT_CONFIG)
988)     try:
989)         os.replace(old_filename, new_filename)
990)     except OSError as exc:
991)         return data, exc
992)     else:
993)         return data, None
994) 
995) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

998) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1001) 
1002)     Args:
1003)         config:
1004)             vault configuration to save.
1005) 
1006)     Raises:
1007)         OSError:
1008)             There was an OS error accessing or writing the file.
1009)         ValueError:
1010)             The data cannot be stored as a vault(1)-compatible config.
1011) 
1012)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1016)     filedir = os.path.dirname(os.path.abspath(filename))
1017)     try:
1018)         os.makedirs(filedir, exist_ok=False)
1019)     except FileExistsError:
1020)         if not os.path.isdir(filedir):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1023)         json.dump(config, fileobj)
1024) 
1025) 
Marco Ricci Introduce a central user co...

Marco Ricci authored 3 weeks ago

1026) def _load_user_config() -> dict[str, Any]:
1027)     """Load the user config from the application directory.
1028) 
1029)     The filename is obtained via [`_config_filename`][].
1030) 
1031)     Returns:
1032)         The user configuration, as a nested `dict`.
1033) 
1034)     Raises:
1035)         OSError:
1036)             There was an OS error accessing the file.
1037)         ValueError:
1038)             The data loaded from the file is not a valid configuration
1039)             file.
1040) 
1041)     """
1042)     filename = _config_filename(subsystem='user configuration')
1043)     with open(filename, 'rb') as fileobj:
1044)         return tomllib.load(fileobj)
1045) 
1046) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1054) 
1055)     Args:
1056)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

1059) 
1060)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1063) 
1064)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1065)         KeyError:
1066)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
1067)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1082) 
1083)     """
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

1089)         suitable_keys = copy.copy(all_key_comment_pairs)
1090)         for pair in all_key_comment_pairs:
1091)             key, _comment = pair
1092)             if vault.Vault.is_suitable_ssh_key(key, client=client):
1093)                 yield pair
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1096) 
1097) 
1098) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1101)     single_choice_prompt: str = 'Confirm this choice?',
1102) ) -> int:
1103)     """Prompt user for a choice among the given items.
1104) 
1105)     Print the heading, if any, then present the items to the user.  If
1106)     there are multiple items, prompt the user for a selection, validate
1107)     the choice, then return the list index of the selected item.  If
1108)     there is only a single item, request confirmation for that item
1109)     instead, and return the correct index.
1110) 
1111)     Args:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1114)         heading:
1115)             A heading for the list of items, to print immediately
1116)             before.  Defaults to a reasonable standard heading.  If
1117)             explicitly empty, print no heading.
1118)         single_choice_prompt:
1119)             The confirmation prompt if there is only a single possible
1120)             choice.  Defaults to a reasonable standard prompt.
1121) 
1122)     Returns:
1123)         An index into the items sequence, indicating the user's
1124)         selection.
1125) 
1126)     Raises:
1127)         IndexError:
1128)             The user made an invalid or empty selection, or requested an
1129)             abort.
1130) 
1131)     """
1132)     n = len(items)
1133)     if heading:
1134)         click.echo(click.style(heading, bold=True))
1135)     for i, x in enumerate(items, start=1):
1136)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
1137)         click.echo(' ', nl=False)
1138)         click.echo(x)
1139)     if n > 1:
1140)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
1141)         choice = click.prompt(
1142)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1143)             err=True,
1144)             type=choices,
1145)             show_choices=False,
1146)             show_default=False,
1147)             default='',
1148)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1152)     prompt_suffix = (
1153)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
1154)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1155)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1156)         click.confirm(
1157)             single_choice_prompt,
1158)             prompt_suffix=prompt_suffix,
1159)             err=True,
1160)             abort=True,
1161)             default=False,
1162)             show_default=False,
1163)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1164)     except click.Abort:
1165)         raise IndexError(_EMPTY_SELECTION) from None
1166)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1167) 
1168) 
1169) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1177) 
1178)     Args:
1179)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

1182) 
1183)     Returns:
1184)         The selected SSH key.
1185) 
1186)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1187)         KeyError:
1188)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
1189)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1197)         IndexError:
1198)             The user made an invalid or empty selection, or requested an
1199)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1207)     """
1208)     suitable_keys = list(_get_suitable_ssh_keys(conn))
1209)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

1214)         remaining_key_display_length = KEY_DISPLAY_LENGTH - 1 - len(keytype)
1215)         key_extract = min(
1216)             key_str,
1217)             '...' + key_str[-remaining_key_display_length:],
1218)             key=len,
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1223)         key_listing,
1224)         heading='Suitable SSH keys:',
1225)         single_choice_prompt='Use this key?',
1226)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1227)     return suitable_keys[choice].key
1228) 
1229) 
1230) def _prompt_for_passphrase() -> str:
1231)     """Interactively prompt for the passphrase.
1232) 
1233)     Calls [`click.prompt`][] internally.  Moved into a separate function
1234)     mainly for testing/mocking purposes.
1235) 
1236)     Returns:
1237)         The user input.
1238) 
1239)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

1240)     return cast(
1241)         str,
1242)         click.prompt(
1243)             'Passphrase',
1244)             default='',
1245)             hide_input=True,
1246)             show_default=False,
1247)             err=True,
1248)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1250) 
1251) 
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

1252) def _toml_key(*parts: str) -> str:
1253)     """Return a formatted TOML key, given its parts."""
Marco Ricci Fix formatting, some covera...

Marco Ricci authored 2 weeks ago

1254) 
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

1255)     def escape(string: str) -> str:
1256)         translated = string.translate({
1257)             0: r'\u0000',
1258)             1: r'\u0001',
1259)             2: r'\u0002',
1260)             3: r'\u0003',
1261)             4: r'\u0004',
1262)             5: r'\u0005',
1263)             6: r'\u0006',
1264)             7: r'\u0007',
1265)             8: r'\b',
1266)             9: r'\t',
1267)             10: r'\n',
1268)             11: r'\u000B',
1269)             12: r'\f',
1270)             13: r'\r',
1271)             14: r'\u000E',
1272)             15: r'\u000F',
1273)             ord('"'): r'\"',
1274)             ord('\\'): r'\\',
1275)             127: r'\u007F',
1276)         })
1277)         return f'"{translated}"' if translated != string else string
Marco Ricci Fix formatting, some covera...

Marco Ricci authored 2 weeks ago

1278) 
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

1279)     return '.'.join(map(escape, parts))
1280) 
1281) 
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1282) class _ORIGIN(enum.Enum):
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

1283)     INTERACTIVE: str = 'interactive input'
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1284) 
1285) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1288)     value: dict[str, Any],
1289)     *,
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

1290)     main_config: dict[str, Any],
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1291) ) -> None:
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

1292)     form_key = 'unicode-normalization-form'
1293)     default_form: str = main_config.get('vault', {}).get(
1294)         f'default-{form_key}', 'NFC'
1295)     )
1296)     form_dict: dict[str, dict] = main_config.get('vault', {}).get(form_key, {})
1297)     form: Any = (
1298)         default_form
1299)         if isinstance(key, _ORIGIN) or key == ('global',)
1300)         else form_dict.get(key[1], default_form)
1301)     )
1302)     config_key = (
1303)         _toml_key('vault', key[1], form_key)
1304)         if isinstance(key, tuple) and len(key) > 1 and key[1] in form_dict
1305)         else f'vault.default-{form_key}'
1306)     )
1307)     if form not in {'NFC', 'NFD', 'NFKC', 'NFKD'}:
1308)         msg = f'Invalid value {form!r} for config key {config_key}'
1309)         raise AssertionError(msg)
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1310)     logger = logging.getLogger(PROG_NAME)
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

1311)     formatted_key = (
1312)         key.value if isinstance(key, _ORIGIN) else _types.json_path(key)
1313)     )
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1314)     if 'phrase' in value:
1315)         phrase = value['phrase']
1316)         if not unicodedata.is_normalized(form, phrase):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

1318)                 (
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

1319)                     'the %s passphrase is not %s-normalized.  '
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1320)                     'Make sure to double-check this is really the '
1321)                     'passphrase you want.'
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

1323)                 formatted_key,
1324)                 form,
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

1325)                 stacklevel=2,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1326)             )
1327) 
1328) 
Marco Ricci Hoist and add tests for int...

Marco Ricci authored 3 weeks ago

1329) def _key_to_phrase(
1330)     key_: str | bytes | bytearray,
1331)     /,
1332)     *,
1333)     error_callback: Callable[..., NoReturn] = sys.exit,
1334) ) -> bytes | bytearray:
1335)     key = base64.standard_b64decode(key_)
1336)     try:
1337)         with ssh_agent.SSHAgentClient.ensure_agent_subcontext() as client:
1338)             try:
1339)                 return vault.Vault.phrase_from_key(key, conn=client)
1340)             except ssh_agent.SSHAgentFailedError as e:
1341)                 try:
1342)                     keylist = client.list_keys()
1343)                 except ssh_agent.SSHAgentFailedError:
1344)                     pass
1345)                 except Exception as e2:  # noqa: BLE001
1346)                     e.__context__ = e2
1347)                 else:
1348)                     if not any(  # pragma: no branch
1349)                         k == key for k, _ in keylist
1350)                     ):
1351)                         error_callback(
1352)                             'The requested SSH key is not loaded '
1353)                             'into the agent.'
1354)                         )
1355)                 error_callback(e)
1356)     except KeyError:
1357)         error_callback('Cannot find running SSH agent; check SSH_AUTH_SOCK')
1358)     except NotImplementedError:
1359)         error_callback(
1360)             'Cannot connect to SSH agent because '
1361)             'this Python version does not support UNIX domain sockets'
1362)         )
1363)     except OSError as e:
1364)         error_callback('Cannot connect to SSH agent: %s', e.strerror)
1365) 
1366) 
Marco Ricci Support exporting the `vaul...

Marco Ricci authored 2 weeks ago

1367) def _print_config_as_sh_script(
1368)     config: _types.VaultConfig,
1369)     /,
1370)     *,
1371)     outfile: TextIO,
1372)     prog_name_list: Sequence[str],
1373) ) -> None:
1374)     service_keys = (
1375)         'length',
1376)         'repeat',
1377)         'lower',
1378)         'upper',
1379)         'number',
1380)         'space',
1381)         'dash',
1382)         'symbol',
1383)     )
1384)     print('#!/bin/sh -e', file=outfile)
1385)     print(file=outfile)
1386)     print(shlex.join([*prog_name_list, '--clear']), file=outfile)
1387)     sv_obj_pairs: list[
1388)         tuple[
1389)             str | None,
1390)             _types.VaultConfigGlobalSettings
1391)             | _types.VaultConfigServicesSettings,
1392)         ],
1393)     ] = list(config['services'].items())
1394)     if config.get('global', {}):
1395)         sv_obj_pairs.insert(0, (None, config['global']))
1396)     for sv, sv_obj in sv_obj_pairs:
1397)         this_service_keys = tuple(k for k in service_keys if k in sv_obj)
1398)         this_other_keys = tuple(k for k in sv_obj if k not in service_keys)
1399)         if this_other_keys:
1400)             other_sv_obj = {k: sv_obj[k] for k in this_other_keys}  # type: ignore[literal-required]
1401)             dumped_config = json.dumps(
1402)                 (
1403)                     {'services': {sv: other_sv_obj}}
1404)                     if sv is not None
1405)                     else {'global': other_sv_obj, 'services': {}}
1406)                 ),
1407)                 ensure_ascii=False,
1408)                 indent=None,
1409)             )
1410)             print(
1411)                 shlex.join([*prog_name_list, '--import', '-']) + " <<'HERE'",
1412)                 dumped_config,
1413)                 'HERE',
1414)                 sep='\n',
1415)                 file=outfile,
1416)             )
1417)         if not this_service_keys and not this_other_keys and sv:
1418)             dumped_config = json.dumps(
1419)                 {'services': {sv: {}}},
1420)                 ensure_ascii=False,
1421)                 indent=None,
1422)             )
1423)             print(
1424)                 shlex.join([*prog_name_list, '--import', '-']) + " <<'HERE'",
1425)                 dumped_config,
1426)                 'HERE',
1427)                 sep='\n',
1428)                 file=outfile,
1429)             )
1430)         elif this_service_keys:
1431)             tokens = [*prog_name_list, '--config']
1432)             for key in this_service_keys:
1433)                 tokens.extend([f'--{key}', str(sv_obj[key])])  # type: ignore[literal-required]
1434)             if sv is not None:
1435)                 tokens.extend(['--', sv])
1436)             print(shlex.join(tokens), file=outfile)
1437) 
1438) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1449) 
1450) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1458) 
1459) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1466)     """
1467) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 weeks ago

1469) class CompatibilityOption(OptionGroupOption):
1470)     """Compatibility and incompatibility options for the CLI."""
1471) 
1472)     option_group_name = 'Options concerning compatibility with other tools'
1473) 
1474) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1475) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1480)     """Check that the occurrence constraint is valid (int, 0 or larger).
1481) 
1482)     Args:
1483)         ctx: The `click` context.
1484)         param: The current command-line parameter.
1485)         value: The parameter value to be checked.
1486) 
1487)     Returns:
1488)         The parsed parameter value.
1489) 
1490)     Raises:
1491)         click.BadParameter: The parameter value is invalid.
1492) 
1493)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1496)     if value is None:
1497)         return value
1498)     if isinstance(value, int):
1499)         int_value = value
1500)     else:
1501)         try:
1502)             int_value = int(value, 10)
1503)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1509)     return int_value
1510) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1511) 
1512) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1517)     """Check that the length is valid (int, 1 or larger).
1518) 
1519)     Args:
1520)         ctx: The `click` context.
1521)         param: The current command-line parameter.
1522)         value: The parameter value to be checked.
1523) 
1524)     Returns:
1525)         The parsed parameter value.
1526) 
1527)     Raises:
1528)         click.BadParameter: The parameter value is invalid.
1529) 
1530)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1533)     if value is None:
1534)         return value
1535)     if isinstance(value, int):
1536)         int_value = value
1537)     else:
1538)         try:
1539)             int_value = int(value, 10)
1540)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1546)     return int_value
1547) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1548) 
1549) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1550) # Enter notes below the line with the cut mark (ASCII scissors and
1551) # dashes).  Lines above the cut mark (such as this one) will be ignored.
1552) #
1553) # If you wish to clear the notes, leave everything beyond the cut mark
1554) # blank.  However, if you leave the *entire* file blank, also removing
1555) # the cut mark, then the edit is aborted, and the old notes contents are
1556) # retained.
1557) #
1558) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1560) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
1561) 
1562) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

1563) @derivepassphrase.command(
1564)     'vault',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1576)     """,
1577) )
1578) @click.option(
1579)     '-p',
1580)     '--phrase',
1581)     'use_phrase',
1582)     is_flag=True,
1583)     help='prompts you for your passphrase',
1584)     cls=PasswordGenerationOption,
1585) )
1586) @click.option(
1587)     '-k',
1588)     '--key',
1589)     'use_key',
1590)     is_flag=True,
1591)     help='uses your SSH private key to generate passwords',
1592)     cls=PasswordGenerationOption,
1593) )
1594) @click.option(
1595)     '-l',
1596)     '--length',
1597)     metavar='NUMBER',
1598)     callback=_validate_length,
1599)     help='emits password of length NUMBER',
1600)     cls=PasswordGenerationOption,
1601) )
1602) @click.option(
1603)     '-r',
1604)     '--repeat',
1605)     metavar='NUMBER',
1606)     callback=_validate_occurrence_constraint,
1607)     help='allows maximum of NUMBER repeated adjacent chars',
1608)     cls=PasswordGenerationOption,
1609) )
1610) @click.option(
1611)     '--lower',
1612)     metavar='NUMBER',
1613)     callback=_validate_occurrence_constraint,
1614)     help='includes at least NUMBER lowercase letters',
1615)     cls=PasswordGenerationOption,
1616) )
1617) @click.option(
1618)     '--upper',
1619)     metavar='NUMBER',
1620)     callback=_validate_occurrence_constraint,
1621)     help='includes at least NUMBER uppercase letters',
1622)     cls=PasswordGenerationOption,
1623) )
1624) @click.option(
1625)     '--number',
1626)     metavar='NUMBER',
1627)     callback=_validate_occurrence_constraint,
1628)     help='includes at least NUMBER digits',
1629)     cls=PasswordGenerationOption,
1630) )
1631) @click.option(
1632)     '--space',
1633)     metavar='NUMBER',
1634)     callback=_validate_occurrence_constraint,
1635)     help='includes at least NUMBER spaces',
1636)     cls=PasswordGenerationOption,
1637) )
1638) @click.option(
1639)     '--dash',
1640)     metavar='NUMBER',
1641)     callback=_validate_occurrence_constraint,
1642)     help='includes at least NUMBER "-" or "_"',
1643)     cls=PasswordGenerationOption,
1644) )
1645) @click.option(
1646)     '--symbol',
1647)     metavar='NUMBER',
1648)     callback=_validate_occurrence_constraint,
1649)     help='includes at least NUMBER symbol chars',
1650)     cls=PasswordGenerationOption,
1651) )
1652) @click.option(
1653)     '-n',
1654)     '--notes',
1655)     'edit_notes',
1656)     is_flag=True,
1657)     help='spawn an editor to edit notes for SERVICE',
1658)     cls=ConfigurationOption,
1659) )
1660) @click.option(
1661)     '-c',
1662)     '--config',
1663)     'store_config_only',
1664)     is_flag=True,
1665)     help='saves the given settings for SERVICE or global',
1666)     cls=ConfigurationOption,
1667) )
1668) @click.option(
1669)     '-x',
1670)     '--delete',
1671)     'delete_service_settings',
1672)     is_flag=True,
1673)     help='deletes settings for SERVICE',
1674)     cls=ConfigurationOption,
1675) )
1676) @click.option(
1677)     '--delete-globals',
1678)     is_flag=True,
1679)     help='deletes the global shared settings',
1680)     cls=ConfigurationOption,
1681) )
1682) @click.option(
1683)     '-X',
1684)     '--clear',
1685)     'clear_all_settings',
1686)     is_flag=True,
1687)     help='deletes all settings',
1688)     cls=ConfigurationOption,
1689) )
1690) @click.option(
1691)     '-e',
1692)     '--export',
1693)     'export_settings',
1694)     metavar='PATH',
1695)     help='export all saved settings into file PATH',
1696)     cls=StorageManagementOption,
1697) )
1698) @click.option(
1699)     '-i',
1700)     '--import',
1701)     'import_settings',
1702)     metavar='PATH',
1703)     help='import saved settings from file PATH',
1704)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 weeks ago

1706) @click.option(
1707)     '--overwrite-existing/--merge-existing',
1708)     'overwrite_config',
1709)     default=False,
1710)     help='overwrite or merge (default) the existing configuration',
1711)     cls=CompatibilityOption,
1712) )
Marco Ricci Allow unsetting settings wh...

Marco Ricci authored 2 weeks ago

1713) @click.option(
1714)     '--unset',
1715)     'unset_settings',
1716)     multiple=True,
1717)     type=click.Choice([
1718)         'phrase',
1719)         'key',
1720)         'length',
1721)         'repeat',
1722)         'lower',
1723)         'upper',
1724)         'number',
1725)         'space',
1726)         'dash',
1727)         'symbol',
1728)     ]),
1729)     help=(
1730)         'with --config, also unsets the given setting; '
1731)         'may be specified multiple times'
1732)     ),
1733)     cls=CompatibilityOption,
1734) )
Marco Ricci Support exporting the `vaul...

Marco Ricci authored 2 weeks ago

1735) @click.option(
1736)     '--export-as',
1737)     type=click.Choice(['JSON', 'sh']),
1738)     default='JSON',
1739)     help='when exporting, export as JSON (default) or POSIX sh',
1740)     cls=CompatibilityOption,
1741) )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1747)     ctx: click.Context,
1748)     /,
1749)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1750)     service: str | None = None,
1751)     use_phrase: bool = False,
1752)     use_key: bool = False,
1753)     length: int | None = None,
1754)     repeat: int | None = None,
1755)     lower: int | None = None,
1756)     upper: int | None = None,
1757)     number: int | None = None,
1758)     space: int | None = None,
1759)     dash: int | None = None,
1760)     symbol: int | None = None,
1761)     edit_notes: bool = False,
1762)     store_config_only: bool = False,
1763)     delete_service_settings: bool = False,
1764)     delete_globals: bool = False,
1765)     clear_all_settings: bool = False,
1766)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
1767)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1768)     overwrite_config: bool = False,
Marco Ricci Allow unsetting settings wh...

Marco Ricci authored 2 weeks ago

1769)     unset_settings: Sequence[str] = (),
Marco Ricci Support exporting the `vaul...

Marco Ricci authored 2 weeks ago

1770)     export_as: Literal['json', 'sh'] = 'json',
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1774)     Using a master passphrase or a master SSH key, derive a passphrase
1775)     for SERVICE, subject to length, character and character repetition
1776)     constraints.  The derivation is cryptographically strong, meaning
1777)     that even if a single passphrase is compromised, guessing the master
1778)     passphrase or a different service's passphrase is computationally
1779)     infeasible.  The derivation is also deterministic, given the same
1780)     inputs, thus the resulting passphrase need not be stored explicitly.
1781)     The service name and constraints themselves also need not be kept
1782)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1794) 
1795)     Parameters:
1796)         ctx (click.Context):
1797)             The `click` context.
1798) 
1799)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1852)         export_settings:
1853)             Command-line argument `-e`/`--export`.  If a file object,
1854)             then it must be open for writing and accept `str` inputs.
1855)             Otherwise, a filename to open for writing.  Using `-` for
1856)             standard output is supported.
1857)         import_settings:
1858)             Command-line argument `-i`/`--import`.  If a file object, it
1859)             must be open for reading and yield `str` values.  Otherwise,
1860)             a filename to open for reading.  Using `-` for standard
1861)             input is supported.
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1862)         overwrite_config:
1863)             Command-line arguments `--overwrite-existing` (True) and
1864)             `--merge-existing` (False).  Controls whether config saving
1865)             and config importing overwrite existing configurations, or
1866)             merge them section-wise instead.
Marco Ricci Allow unsetting settings wh...

Marco Ricci authored 2 weeks ago

1867)         unset_settings:
1868)             Command-line argument `--unset`.  If given together with
1869)             `--config`, unsets the specified settings (in addition to
1870)             any other changes requested).
Marco Ricci Support exporting the `vaul...

Marco Ricci authored 2 weeks ago

1871)         export_as:
1872)             Command-line argument `--export-as`.  If given together with
1873)             `--export`, selects the format to export the current
1874)             configuration as: JSON ("json", default) or POSIX sh ("sh").
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

1884)             # Use match/case here once Python 3.9 becomes unsupported.
1885)             if isinstance(param, PasswordGenerationOption):
1886)                 group = PasswordGenerationOption
1887)             elif isinstance(param, ConfigurationOption):
1888)                 group = ConfigurationOption
1889)             elif isinstance(param, StorageManagementOption):
1890)                 group = StorageManagementOption
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 3 months ago

1895)             elif isinstance(param, OptionGroupOption):
1896)                 raise AssertionError(  # noqa: DOC501,TRY003,TRY004
1897)                     f'Unknown option group for {param!r}'  # noqa: EM102
1898)                 )
1899)             else:
1900)                 group = click.Option
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1901)             options_in_group.setdefault(group, []).append(param)
1902)         params_by_str[param.human_readable_name] = param
1903)         for name in param.opts + param.secondary_opts:
1904)             params_by_str[name] = param
1905) 
Marco Ricci Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

1906)     @functools.cache
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1910)     def check_incompatible_options(
Marco Ricci Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

1911)         param1: click.Parameter | str,
1912)         param2: click.Parameter | str,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1913)     ) -> None:
Marco Ricci Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

1914)         param1 = params_by_str[param1] if isinstance(param1, str) else param1
1915)         param2 = params_by_str[param2] if isinstance(param2, str) else param2
1916)         if param1 == param2:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1917)             return
Marco Ricci Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

1918)         if not is_param_set(param1):
1919)             return
1920)         if is_param_set(param2):
1921)             param1_str = param1.human_readable_name
1922)             param2_str = param2.human_readable_name
1923)             raise click.BadOptionUsage(
1924)                 param1_str, f'mutually exclusive with {param2_str}', ctx=ctx
1925)             )
1926)         return
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

1932)         ctx.exit(1)
1933) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1935)         try:
1936)             return _load_config()
1937)         except FileNotFoundError:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1938)             try:
1939)                 backup_config, exc = _migrate_and_load_old_config()
1940)             except FileNotFoundError:
1941)                 return {'services': {}}
Marco Ricci Make obtaining the compatib...

Marco Ricci authored 3 weeks ago

1942)             old_name = os.path.basename(
1943)                 _config_filename(subsystem='old settings.json')
1944)             )
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

1953)                 old_name,
1954)                 new_name,
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

1957)                 logger.warning(
1958)                     'Failed to migrate to %r: %s: %r',
1959)                     new_name,
1960)                     exc.strerror,
1961)                     exc.filename,
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1962)                 )
1963)             else:
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

1970) 
1971)     def put_config(config: _types.VaultConfig, /) -> None:
1972)         try:
1973)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

1978) 
Marco Ricci Introduce a central user co...

Marco Ricci authored 3 weeks ago

1979)     def get_user_config() -> dict[str, Any]:
1980)         try:
1981)             return _load_user_config()
1982)         except FileNotFoundError:
1983)             return {}
1984)         except OSError as e:
1985)             err('Cannot load user config: %s: %r', e.strerror, e.filename)
1986)         except Exception as e:  # noqa: BLE001
1987)             err('Cannot load user config: %s', str(e), exc_info=e)
1988) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1990) 
1991)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1993)         for opt in options_in_group[group]:
1994)             if opt != params_by_str['--config']:
Marco Ricci Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

1995)                 for other_opt in options_in_group[PasswordGenerationOption]:
1996)                     check_incompatible_options(opt, other_opt)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1999)         for opt in options_in_group[group]:
Marco Ricci Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

2000)             for other_opt in options_in_group[ConfigurationOption]:
2001)                 check_incompatible_options(opt, other_opt)
2002)             for other_opt in options_in_group[StorageManagementOption]:
2003)                 check_incompatible_options(opt, other_opt)
Marco Ricci Correctly model vault globa...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 2 months ago

2011)             raise click.UsageError(msg)  # noqa: DOC501
2012)     sv_options = [params_by_str['--notes'], params_by_str['--delete']]
2013)     for param in sv_options:
2014)         if is_param_set(param) and not service:
2015)             opt_str = param.opts[0]
2016)             msg = f'{opt_str} requires a SERVICE'
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

2018)     no_sv_options = [
2019)         params_by_str['--delete-globals'],
2020)         params_by_str['--clear'],
2021)         *options_in_group[StorageManagementOption],
2022)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

2028) 
Marco Ricci Introduce a central user co...

Marco Ricci authored 3 weeks ago

2029)     user_config = get_user_config()
2030) 
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

2039)     if edit_notes:
2040)         assert service is not None
2041)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2048)             while notes_lines:
2049)                 line = notes_lines.popleft()
2050)                 if line.startswith(DEFAULT_NOTES_MARKER):
2051)                     notes_value = ''.join(notes_lines)
2052)                     break
2053)             else:
2054)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

2057)                 notes_value.strip('\n')
2058)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2060)     elif delete_service_settings:
2061)         assert service is not None
2062)         configuration = get_config()
2063)         if service in configuration['services']:
2064)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2066)     elif delete_globals:
2067)         configuration = get_config()
2068)         if 'global' in configuration:
2069)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2073)     elif import_settings:
2074)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

2075)             # TODO(the-13th-letter): keep track of auto-close; try
2076)             # os.dup if feasible
Marco Ricci Document handling of file o...

Marco Ricci authored 3 weeks ago

2077)             infile = cast(
2078)                 TextIO,
2079)                 (
2080)                     import_settings
2081)                     if hasattr(import_settings, 'close')
2082)                     else click.open_file(os.fspath(import_settings), 'rt')
2083)                 ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

2084)             )
Marco Ricci Document handling of file o...

Marco Ricci authored 3 weeks ago

2085)             # Don't specifically catch TypeError or ValueError here if
2086)             # the passed-in fileobj is not a readable text stream.  This
2087)             # will never happen on the command-line (thanks to `click`),
2088)             # and for programmatic use, our caller may want accurate
2089)             # error information.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2090)             with infile:
2091)                 maybe_config = json.load(infile)
2092)         except json.JSONDecodeError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

2099)         assert cleaned is not None
2100)         for step in cleaned:
2101)             # These are never fatal errors, because the semantics of
2102)             # vault upon encountering these settings are ill-specified,
2103)             # but not ill-defined.
2104)             if step.action == 'replace':
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

2105)                 logger.warning(
2106)                     'Replacing invalid value %s for key %s with %s.',
2107)                     json.dumps(step.old_value),
2108)                     _types.json_path(step.path),
2109)                     json.dumps(step.new_value),
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

2110)                 )
2111)             else:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

2112)                 logger.warning(
2113)                     'Removing ineffective setting %s = %s.',
2114)                     _types.json_path(step.path),
2115)                     json.dumps(step.old_value),
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

2118)             logger.warning(
2119)                 (
2120)                     'An empty SERVICE is not supported by vault(1), '
2121)                     'and the empty-string service settings will be '
2122)                     'inaccessible and ineffective.  To ensure that '
2123)                     'vault(1) and %s see the settings, move them '
2124)                     'into the "global" section.'
2125)                 ),
2126)                 PROG_NAME,
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

2127)             )
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

2128)         try:
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

2129)             _check_for_misleading_passphrase(
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

2130)                 ('global',),
2131)                 cast(dict[str, Any], maybe_config.get('global', {})),
2132)                 main_config=user_config,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

2133)             )
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

2134)             for key, value in maybe_config['services'].items():
2135)                 _check_for_misleading_passphrase(
2136)                     ('services', key),
2137)                     cast(dict[str, Any], value),
2138)                     main_config=user_config,
2139)                 )
2140)         except AssertionError as e:
2141)             err('The configuration file is invalid.  ' + str(e))
Marco Ricci Fix empty key handling in `...

Marco Ricci authored 2 weeks ago

2142)         global_obj = maybe_config.get('global', {})
2143)         has_key = _types.js_truthiness(global_obj.get('key'))
2144)         has_phrase = _types.js_truthiness(global_obj.get('phrase'))
2145)         if has_key and has_phrase:
2146)             logger.warning(
2147)                 'Setting a global passphrase is ineffective '
2148)                 'because a key is also set.'
2149)             )
2150)         for service_name, service_obj in maybe_config['services'].items():
2151)             has_key = _types.js_truthiness(
2152)                 service_obj.get('key')
2153)             ) or _types.js_truthiness(global_obj.get('key'))
2154)             has_phrase = _types.js_truthiness(
2155)                 service_obj.get('phrase')
2156)             ) or _types.js_truthiness(global_obj.get('phrase'))
2157)             if has_key and has_phrase:
2158)                 logger.warning(
2159)                     (
2160)                         'Setting a service passphrase is ineffective '
2161)                         'because a key is also set: %s'
2162)                     ),
2163)                     json.dumps(service_name),
2164)                 )
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

2165)         if overwrite_config:
2166)             put_config(maybe_config)
2167)         else:
2168)             configuration = get_config()
2169)             merged_config: collections.ChainMap[str, Any] = (
2170)                 collections.ChainMap(
2171)                     {
2172)                         'services': collections.ChainMap(
2173)                             maybe_config['services'],
2174)                             configuration['services'],
2175)                         ),
2176)                     },
2177)                     {'global': maybe_config['global']}
2178)                     if 'global' in maybe_config
2179)                     else {},
2180)                     {'global': configuration['global']}
2181)                     if 'global' in configuration
2182)                     else {},
2183)                 )
2184)             )
2185)             new_config: Any = {
2186)                 k: dict(v) if isinstance(v, collections.ChainMap) else v
2187)                 for k, v in sorted(merged_config.items())
2188)             }
2189)             assert _types.is_vault_config(new_config)
2190)             put_config(new_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2191)     elif export_settings:
2192)         configuration = get_config()
2193)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

2194)             # TODO(the-13th-letter): keep track of auto-close; try
2195)             # os.dup if feasible
Marco Ricci Document handling of file o...

Marco Ricci authored 3 weeks ago

2196)             outfile = cast(
2197)                 TextIO,
2198)                 (
2199)                     export_settings
2200)                     if hasattr(export_settings, 'close')
2201)                     else click.open_file(os.fspath(export_settings), 'wt')
2202)                 ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

2203)             )
Marco Ricci Document handling of file o...

Marco Ricci authored 3 weeks ago

2204)             # Don't specifically catch TypeError or ValueError here if
2205)             # the passed-in fileobj is not a writable text stream.  This
2206)             # will never happen on the command-line (thanks to `click`),
2207)             # and for programmatic use, our caller may want accurate
2208)             # error information.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2209)             with outfile:
Marco Ricci Support exporting the `vaul...

Marco Ricci authored 2 weeks ago

2210)                 if export_as == 'sh':
2211)                     this_ctx = ctx
2212)                     prog_name_pieces = collections.deque([
2213)                         this_ctx.info_name or 'vault',
2214)                     ])
2215)                     while (
2216)                         this_ctx.parent is not None
2217)                         and this_ctx.parent.info_name is not None
2218)                     ):
2219)                         prog_name_pieces.appendleft(this_ctx.parent.info_name)
2220)                         this_ctx = this_ctx.parent
2221)                     _print_config_as_sh_script(
2222)                         configuration,
2223)                         outfile=outfile,
2224)                         prog_name_list=prog_name_pieces,
2225)                     )
2226)                 else:
2227)                     json.dump(configuration, outfile)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

2237)         service_keys = {
2238)             'key',
2239)             'phrase',
2240)             'length',
2241)             'repeat',
2242)             'lower',
2243)             'upper',
2244)             'number',
2245)             'space',
2246)             'dash',
2247)             'symbol',
2248)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

2250)             {
2251)                 k: v
2252)                 for k, v in locals().items()
2253)                 if k in service_keys and v is not None
2254)             },
2255)             cast(
2256)                 dict[str, Any],
2257)                 configuration['services'].get(service or '', {}),
2258)             ),
2259)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2260)         )
2261)         if use_key:
2262)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

2263)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
2264)                     'ASCII'
2265)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

2270)             except NotImplementedError:
2271)                 err(
2272)                     'Cannot connect to SSH agent because '
2273)                     'this Python version does not support UNIX domain sockets'
2274)                 )
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

2277)             except (
2278)                 LookupError,
2279)                 RuntimeError,
2280)                 ssh_agent.SSHAgentFailedError,
2281)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2283)         elif use_phrase:
2284)             maybe_phrase = _prompt_for_passphrase()
2285)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2287)             else:
2288)                 phrase = maybe_phrase
2289)         if store_config_only:
2290)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

2291)             view = (
2292)                 collections.ChainMap(*settings.maps[:2])
2293)                 if service
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

2296)             if use_key:
2297)                 view['key'] = key
2298)             elif use_phrase:
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

2299)                 view['phrase'] = phrase
2300)                 settings_type = 'service' if service else 'global'
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

2301)                 try:
2302)                     _check_for_misleading_passphrase(
2303)                         ('services', service) if service else ('global',),
2304)                         {'phrase': phrase},
2305)                         main_config=user_config,
2306)                     )
2307)                 except AssertionError as e:
2308)                     err('The configuration file is invalid.  ' + str(e))
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

2309)                 if 'key' in settings:
Marco Ricci Fix empty key handling in `...

Marco Ricci authored 2 weeks ago

2310)                     if service:
2311)                         logger.warning(
2312)                             (
2313)                                 'Setting a service passphrase is ineffective '
2314)                                 'because a key is also set: %s'
2315)                             ),
2316)                             json.dumps(service),
2317)                         )
2318)                     else:
2319)                         logger.warning(
2320)                             'Setting a global passphrase is ineffective '
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

2321)                             'because a key is also set.'
Marco Ricci Fix empty key handling in `...

Marco Ricci authored 2 weeks ago

2322)                         )
Marco Ricci Allow unsetting settings wh...

Marco Ricci authored 2 weeks ago

2323)             if not view.maps[0] and not unset_settings:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

2324)                 settings_type = 'service' if service else 'global'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

2327)                     f'actual settings'
2328)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

2329)                 raise click.UsageError(msg)
Marco Ricci Allow unsetting settings wh...

Marco Ricci authored 2 weeks ago

2330)             for setting in unset_settings:
2331)                 if setting in view.maps[0]:
2332)                     msg = (
2333)                         f'Attempted to unset and set --{setting} '
2334)                         f'at the same time.'
2335)                     )
2336)                     raise click.UsageError(msg)
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

2337)             subtree: dict[str, Any] = (
2338)                 configuration['services'].setdefault(service, {})  # type: ignore[assignment]
2339)                 if service
2340)                 else configuration.setdefault('global', {})
2341)             )
2342)             if overwrite_config:
2343)                 subtree.clear()
Marco Ricci Allow unsetting settings wh...

Marco Ricci authored 2 weeks ago

2344)             else:
2345)                 for setting in unset_settings:
2346)                     subtree.pop(setting, None)
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

2347)             subtree.update(view)
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2352)         else:
2353)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

2356)             kwargs: dict[str, Any] = {
2357)                 k: v
2358)                 for k, v in settings.items()
2359)                 if k in service_keys and v is not None
2360)             }
2361) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

2362)             if use_phrase:
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

2363)                 try:
2364)                     _check_for_misleading_passphrase(
2365)                         _ORIGIN.INTERACTIVE,
2366)                         {'phrase': phrase},
2367)                         main_config=user_config,
2368)                     )
2369)                 except AssertionError as e:
2370)                     err('The configuration file is invalid.  ' + str(e))
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

2378)             if use_key or use_phrase:
Marco Ricci Fix formatting, some covera...

Marco Ricci authored 2 weeks ago

2379)                 kwargs['phrase'] = (
2380)                     _key_to_phrase(key, error_callback=err)
2381)                     if use_key
2382)                     else phrase
2383)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2384)             elif kwargs.get('key'):
Marco Ricci Hoist and add tests for int...

Marco Ricci authored 3 weeks ago

2385)                 kwargs['phrase'] = _key_to_phrase(
2386)                     kwargs['key'], error_callback=err
2387)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2388)             elif kwargs.get('phrase'):
2389)                 pass
2390)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

2393)                     'or in configuration'
2394)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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