208429075c67c884f55d76474e8c8a4baea616ab
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
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

149)             [`logging.CRITICAL`][] `"Error: "`, `LABEL` is the empty
150)             string.
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

151) 
152)         The level indication strings at level `WARNING` or above are
153)         highlighted.  Use [`click.echo`][] to output them and remove
154)         color output if necessary.
155) 
156)         Args:
157)             record: A log record.
158) 
159)         Returns:
160)             A formatted log record.
161) 
162)         Raises:
163)             AssertionError:
164)                 The log level is not supported.
165) 
166)         """
167)         preliminary_result = record.getMessage()
168)         prefix = f'{self.prog_name}: '
169)         if record.levelname == 'DEBUG':  # pragma: no cover
170)             level_indicator = 'Debug: '
171)         elif record.levelname == 'INFO':
172)             level_indicator = ''
173)         elif record.levelname == 'WARNING':
174)             level_indicator = (
175)                 f'{click.style("Deprecation warning", bold=True)}: '
176)                 if record.name.endswith('.deprecation')
177)                 else f'{click.style("Warning", bold=True)}: '
178)             )
179)         elif record.levelname in {'ERROR', 'CRITICAL'}:
180)             level_indicator = ''
181)         else:  # pragma: no cover
182)             msg = f'Unsupported logging level: {record.levelname}'
183)             raise AssertionError(msg)
184)         return ''.join(
185)             prefix + level_indicator + line
186)             for line in preliminary_result.splitlines(True)  # noqa: FBT003
187)         )
188) 
189) 
190) class StandardCLILogging:
191)     """Set up CLI logging handlers upon instantiation."""
192) 
193)     prog_name = PROG_NAME
194)     package_name = PROG_NAME.lower().replace(' ', '_').replace('-', '_')
195)     cli_formatter = CLIofPackageFormatter(
196)         prog_name=prog_name, package_name=package_name
197)     )
198)     cli_handler = ClickEchoStderrHandler()
199)     cli_handler.addFilter(logging.Filter(name=package_name))
200)     cli_handler.setFormatter(cli_formatter)
201)     cli_handler.setLevel(logging.WARNING)
202)     warnings_handler = ClickEchoStderrHandler()
203)     warnings_handler.addFilter(logging.Filter(name='py.warnings'))
204)     warnings_handler.setFormatter(cli_formatter)
205)     warnings_handler.setLevel(logging.WARNING)
206) 
207)     @classmethod
208)     def ensure_standard_logging(cls) -> StandardLoggingContextManager:
209)         """Return a context manager to ensure standard logging is set up."""
210)         return StandardLoggingContextManager(
211)             handler=cls.cli_handler,
212)             root_logger=cls.package_name,
213)         )
214) 
215)     @classmethod
216)     def ensure_standard_warnings_logging(
217)         cls,
218)     ) -> StandardWarningsLoggingContextManager:
219)         """Return a context manager to ensure warnings logging is set up."""
220)         return StandardWarningsLoggingContextManager(
221)             handler=cls.warnings_handler,
222)         )
223) 
224) 
225) class StandardLoggingContextManager:
226)     """A reentrant context manager setting up standard CLI logging.
227) 
228)     Ensures that the given handler (defaulting to the CLI logging
229)     handler) is added to the named logger (defaulting to the root
230)     logger), and if it had to be added, then that it will be removed
231)     upon exiting the context.
232) 
233)     Reentrant, but not thread safe, because it temporarily modifies
234)     global state.
235) 
236)     """
237) 
238)     def __init__(
239)         self,
240)         handler: logging.Handler,
241)         root_logger: str | None = None,
242)     ) -> None:
243)         self.handler = handler
244)         self.root_logger_name = root_logger
245)         self.base_logger = logging.getLogger(self.root_logger_name)
246)         self.action_required: MutableSequence[bool] = collections.deque()
247) 
248)     def __enter__(self) -> Self:
249)         self.action_required.append(
250)             self.handler not in self.base_logger.handlers
251)         )
252)         if self.action_required[-1]:
253)             self.base_logger.addHandler(self.handler)
254)         return self
255) 
256)     def __exit__(
257)         self,
258)         exc_type: type[BaseException] | None,
259)         exc_value: BaseException | None,
260)         exc_tb: types.TracebackType | None,
261)     ) -> Literal[False]:
262)         if self.action_required[-1]:
263)             self.base_logger.removeHandler(self.handler)
264)         self.action_required.pop()
265)         return False
266) 
267) 
268) class StandardWarningsLoggingContextManager(StandardLoggingContextManager):
269)     """A reentrant context manager setting up standard warnings logging.
270) 
271)     Ensures that warnings are being diverted to the logging system, and
272)     that the given handler (defaulting to the CLI logging handler) is
273)     added to the warnings logger. If the handler had to be added, then
274)     it will be removed upon exiting the context.
275) 
276)     Reentrant, but not thread safe, because it temporarily modifies
277)     global state.
278) 
279)     """
280) 
281)     def __init__(
282)         self,
283)         handler: logging.Handler,
284)     ) -> None:
285)         super().__init__(handler=handler, root_logger='py.warnings')
286)         self.stack: MutableSequence[
287)             tuple[
288)                 Callable[
289)                     [
290)                         type[BaseException] | None,
291)                         BaseException | None,
292)                         types.TracebackType | None,
293)                     ],
294)                     None,
295)                 ],
296)                 Callable[
297)                     [
298)                         str | Warning,
299)                         type[Warning],
300)                         str,
301)                         int,
302)                         TextIO | None,
303)                         str | None,
304)                     ],
305)                     None,
306)                 ],
307)             ]
308)         ] = collections.deque()
309) 
310)     def __enter__(self) -> Self:
311)         def showwarning(  # noqa: PLR0913,PLR0917
312)             message: str | Warning,
313)             category: type[Warning],
314)             filename: str,
315)             lineno: int,
316)             file: TextIO | None = None,
317)             line: str | None = None,
318)         ) -> None:
319)             if file is not None:  # pragma: no cover
320)                 self.stack[0][1](
321)                     message, category, filename, lineno, file, line
322)                 )
323)             else:
324)                 logging.getLogger('py.warnings').warning(
325)                     str(
326)                         warnings.formatwarning(
327)                             message, category, filename, lineno, line
328)                         )
329)                     )
330)                 )
331) 
332)         ctx = warnings.catch_warnings()
333)         exit_func = ctx.__exit__
334)         ctx.__enter__()
335)         self.stack.append((exit_func, warnings.showwarning))
336)         warnings.showwarning = showwarning
337)         return super().__enter__()
338) 
339)     def __exit__(
340)         self,
341)         exc_type: type[BaseException] | None,
342)         exc_value: BaseException | None,
343)         exc_tb: types.TracebackType | None,
344)     ) -> Literal[False]:
345)         ret = super().__exit__(exc_type, exc_value, exc_tb)
346)         val = self.stack.pop()[0](exc_type, exc_value, exc_tb)
347)         assert not val
348)         return ret
349) 
350) 
351) P = ParamSpec('P')
352) R = TypeVar('R')
353) 
354) 
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

516) def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]:
517)     """Decorate the function with standard logging click options.
518) 
519)     Adds the three click options `-v`/`--verbose`, `-q`/`--quiet` and
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

520)     `--debug`, which calls back into the [`adjust_logging_level`][]
521)     function (with different argument values).
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

522) 
523)     Args:
524)         f: A callable to decorate.
525) 
526)     Returns:
527)         The decorated callable.
528) 
529)     """
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

899) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

999) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

1086)         try:
1087)             all_key_comment_pairs = list(client.list_keys())
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1088)         except EOFError as exc:  # pragma: no cover
1089)             raise RuntimeError(_AGENT_COMMUNICATION_ERROR) from exc
Marco Ricci Publish polished `is_suitab...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

1156)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 2 weeks ago

1255) 
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 2 weeks ago

1279) 
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

1319)                 (
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 weeks ago

1330) def _key_to_phrase(
1331)     key_: str | bytes | bytearray,
1332)     /,
1333)     *,
1334)     error_callback: Callable[..., NoReturn] = sys.exit,
1335) ) -> bytes | bytearray:
1336)     key = base64.standard_b64decode(key_)
1337)     try:
1338)         with ssh_agent.SSHAgentClient.ensure_agent_subcontext() as client:
1339)             try:
1340)                 return vault.Vault.phrase_from_key(key, conn=client)
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1341)             except ssh_agent.SSHAgentFailedError as exc:
Marco Ricci Hoist and add tests for int...

Marco Ricci authored 3 weeks ago

1342)                 try:
1343)                     keylist = client.list_keys()
1344)                 except ssh_agent.SSHAgentFailedError:
1345)                     pass
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1346)                 except Exception as exc2:  # noqa: BLE001
1347)                     exc.__context__ = exc2
Marco Ricci Hoist and add tests for int...

Marco Ricci authored 3 weeks ago

1348)                 else:
1349)                     if not any(  # pragma: no branch
1350)                         k == key for k, _ in keylist
1351)                     ):
1352)                         error_callback(
1353)                             'The requested SSH key is not loaded '
1354)                             'into the agent.'
1355)                         )
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1356)                 error_callback(exc)
Marco Ricci Hoist and add tests for int...

Marco Ricci authored 3 weeks ago

1357)     except KeyError:
1358)         error_callback('Cannot find running SSH agent; check SSH_AUTH_SOCK')
1359)     except NotImplementedError:
1360)         error_callback(
1361)             'Cannot connect to SSH agent because '
1362)             'this Python version does not support UNIX domain sockets'
1363)         )
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1364)     except OSError as exc:
1365)         error_callback('Cannot connect to SSH agent: %s', exc.strerror)
Marco Ricci Hoist and add tests for int...

Marco Ricci authored 3 weeks ago

1366) 
1367) 
Marco Ricci Support exporting the `vaul...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 6 months ago

1476) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1497)     if value is None:
1498)         return value
1499)     if isinstance(value, int):
1500)         int_value = value
1501)     else:
1502)         try:
1503)             int_value = int(value, 10)
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1504)         except ValueError as exc:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1505)             msg = 'not an integer'
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1506)             raise click.BadParameter(msg) from exc
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1534)     if value is None:
1535)         return value
1536)     if isinstance(value, int):
1537)         int_value = value
1538)     else:
1539)         try:
1540)             int_value = int(value, 10)
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1541)         except ValueError as exc:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1542)             msg = 'not an integer'
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1543)             raise click.BadParameter(msg) from exc
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

1736) @click.option(
1737)     '--export-as',
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1738)     type=click.Choice(['json', 'sh']),
1739)     default='json',
Marco Ricci Support exporting the `vaul...

Marco Ricci authored 2 weeks ago

1740)     help='when exporting, export as JSON (default) or POSIX sh',
1741)     cls=CompatibilityOption,
1742) )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1909)         return bool(ctx.params.get(param.human_readable_name))
1910) 
Marco Ricci Make the mutually exclusive...

Marco Ricci authored 2 weeks ago

1911)     def option_name(param: click.Parameter | str) -> str:
1912)         # Annoyingly, `param.human_readable_name` contains the *function*
1913)         # parameter name, not the list of option names.  *Those* are
1914)         # stashed in the `.opts` and `.secondary_opts` attributes, which
1915)         # are visible in the `.to_info_dict()` output, but not otherwise
1916)         # documented.
1917)         param = params_by_str[param] if isinstance(param, str) else param
1918)         names = [param.human_readable_name, *param.opts, *param.secondary_opts]
1919)         option_names = [n for n in names if n.startswith('--')]
1920)         return min(option_names, key=len)
1921) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

1926)         param1 = params_by_str[param1] if isinstance(param1, str) else param1
1927)         param2 = params_by_str[param2] if isinstance(param2, str) else param2
1928)         if param1 == param2:
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1929)             return
Marco Ricci Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

1930)         if not is_param_set(param1):
1931)             return
1932)         if is_param_set(param2):
Marco Ricci Make the mutually exclusive...

Marco Ricci authored 2 weeks ago

1933)             param1_str = option_name(param1)
1934)             param2_str = option_name(param2)
Marco Ricci Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

1935)             raise click.BadOptionUsage(
Marco Ricci Make the mutually exclusive...

Marco Ricci authored 2 weeks ago

1936)                 param1_str,
1937)                 f'{param1_str} is mutually exclusive with {param2_str}',
1938)                 ctx=ctx,
Marco Ricci Rewrite incompatible option...

Marco Ricci authored 3 weeks ago

1939)             )
1940)         return
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

1946)         ctx.exit(1)
1947) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1949)         try:
1950)             return _load_config()
1951)         except FileNotFoundError:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1952)             try:
1953)                 backup_config, exc = _migrate_and_load_old_config()
1954)             except FileNotFoundError:
1955)                 return {'services': {}}
Marco Ricci Make obtaining the compatib...

Marco Ricci authored 3 weeks ago

1956)             old_name = os.path.basename(
1957)                 _config_filename(subsystem='old settings.json')
1958)             )
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

1967)                 old_name,
1968)                 new_name,
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

1971)                 logger.warning(
1972)                     'Failed to migrate to %r: %s: %r',
1973)                     new_name,
1974)                     exc.strerror,
1975)                     exc.filename,
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1976)                 )
1977)             else:
Marco Ricci Fix usage of `--debug`, `--...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 3 months ago

1979)             return backup_config
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1980)         except OSError as exc:
1981)             err('Cannot load config: %s: %r', exc.strerror, exc.filename)
1982)         except Exception as exc:  # noqa: BLE001
1983)             err('Cannot load config: %s', str(exc), exc_info=exc)
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1984) 
1985)     def put_config(config: _types.VaultConfig, /) -> None:
1986)         try:
1987)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

1993)     def get_user_config() -> dict[str, Any]:
1994)         try:
1995)             return _load_user_config()
1996)         except FileNotFoundError:
1997)             return {}
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

1998)         except OSError as exc:
1999)             err('Cannot load user config: %s: %r', exc.strerror, exc.filename)
2000)         except Exception as exc:  # noqa: BLE001
2001)             err('Cannot load user config: %s', str(exc), exc_info=exc)
Marco Ricci Introduce a central user co...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

2004) 
2005)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

2014)             for other_opt in options_in_group[ConfigurationOption]:
2015)                 check_incompatible_options(opt, other_opt)
2016)             for other_opt in options_in_group[StorageManagementOption]:
2017)                 check_incompatible_options(opt, other_opt)
Marco Ricci Correctly model vault globa...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 2 months ago

2025)             raise click.UsageError(msg)  # noqa: DOC501
2026)     sv_options = [params_by_str['--notes'], params_by_str['--delete']]
2027)     for param in sv_options:
2028)         if is_param_set(param) and not service:
2029)             opt_str = param.opts[0]
2030)             msg = f'{opt_str} requires a SERVICE'
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

2032)     no_sv_options = [
2033)         params_by_str['--delete-globals'],
2034)         params_by_str['--clear'],
2035)         *options_in_group[StorageManagementOption],
2036)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

2043)     user_config = get_user_config()
2044) 
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

2051)         )
2052) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2053)     if edit_notes:
2054)         assert service is not None
2055)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2062)             while notes_lines:
2063)                 line = notes_lines.popleft()
2064)                 if line.startswith(DEFAULT_NOTES_MARKER):
2065)                     notes_value = ''.join(notes_lines)
2066)                     break
2067)             else:
2068)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

2071)                 notes_value.strip('\n')
2072)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2074)     elif delete_service_settings:
2075)         assert service is not None
2076)         configuration = get_config()
2077)         if service in configuration['services']:
2078)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2080)     elif delete_globals:
2081)         configuration = get_config()
2082)         if 'global' in configuration:
2083)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2087)     elif import_settings:
2088)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 weeks ago

2091)             infile = cast(
2092)                 TextIO,
2093)                 (
2094)                     import_settings
2095)                     if hasattr(import_settings, 'close')
2096)                     else click.open_file(os.fspath(import_settings), 'rt')
2097)                 ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 6 months ago

2104)             with infile:
2105)                 maybe_config = json.load(infile)
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

2106)         except json.JSONDecodeError as exc:
2107)             err('Cannot load config: cannot decode JSON: %s', exc)
2108)         except OSError as exc:
2109)             err('Cannot load config: %s: %r', exc.strerror, exc.filename)
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

2113)         assert cleaned is not None
2114)         for step in cleaned:
2115)             # These are never fatal errors, because the semantics of
2116)             # vault upon encountering these settings are ill-specified,
2117)             # but not ill-defined.
2118)             if step.action == 'replace':
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

2119)                 logger.warning(
2120)                     'Replacing invalid value %s for key %s with %s.',
2121)                     json.dumps(step.old_value),
2122)                     _types.json_path(step.path),
2123)                     json.dumps(step.new_value),
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

2124)                 )
2125)             else:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

2126)                 logger.warning(
2127)                     'Removing ineffective setting %s = %s.',
2128)                     _types.json_path(step.path),
2129)                     json.dumps(step.old_value),
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

2132)             logger.warning(
2133)                 (
2134)                     'An empty SERVICE is not supported by vault(1), '
2135)                     'and the empty-string service settings will be '
2136)                     'inaccessible and ineffective.  To ensure that '
2137)                     'vault(1) and %s see the settings, move them '
2138)                     'into the "global" section.'
2139)                 ),
2140)                 PROG_NAME,
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

2141)             )
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 4 months ago

2147)             )
Marco Ricci Turn Unicode normalization...

Marco Ricci authored 3 weeks ago

2148)             for key, value in maybe_config['services'].items():
2149)                 _check_for_misleading_passphrase(
2150)                     ('services', key),
2151)                     cast(dict[str, Any], value),
2152)                     main_config=user_config,
2153)                 )
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

2154)         except AssertionError as exc:
2155)             err('The configuration file is invalid.  ' + str(exc))
Marco Ricci Fix empty key handling in `...

Marco Ricci authored 3 weeks ago

2156)         global_obj = maybe_config.get('global', {})
2157)         has_key = _types.js_truthiness(global_obj.get('key'))
2158)         has_phrase = _types.js_truthiness(global_obj.get('phrase'))
2159)         if has_key and has_phrase:
2160)             logger.warning(
2161)                 'Setting a global passphrase is ineffective '
2162)                 'because a key is also set.'
2163)             )
2164)         for service_name, service_obj in maybe_config['services'].items():
2165)             has_key = _types.js_truthiness(
2166)                 service_obj.get('key')
2167)             ) or _types.js_truthiness(global_obj.get('key'))
2168)             has_phrase = _types.js_truthiness(
2169)                 service_obj.get('phrase')
2170)             ) or _types.js_truthiness(global_obj.get('phrase'))
2171)             if has_key and has_phrase:
2172)                 logger.warning(
2173)                     (
2174)                         'Setting a service passphrase is ineffective '
2175)                         'because a key is also set: %s'
2176)                     ),
2177)                     json.dumps(service_name),
2178)                 )
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

2179)         if overwrite_config:
2180)             put_config(maybe_config)
2181)         else:
2182)             configuration = get_config()
2183)             merged_config: collections.ChainMap[str, Any] = (
2184)                 collections.ChainMap(
2185)                     {
2186)                         'services': collections.ChainMap(
2187)                             maybe_config['services'],
2188)                             configuration['services'],
2189)                         ),
2190)                     },
2191)                     {'global': maybe_config['global']}
2192)                     if 'global' in maybe_config
2193)                     else {},
2194)                     {'global': configuration['global']}
2195)                     if 'global' in configuration
2196)                     else {},
2197)                 )
2198)             )
2199)             new_config: Any = {
2200)                 k: dict(v) if isinstance(v, collections.ChainMap) else v
2201)                 for k, v in sorted(merged_config.items())
2202)             }
2203)             assert _types.is_vault_config(new_config)
2204)             put_config(new_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2205)     elif export_settings:
2206)         configuration = get_config()
2207)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 weeks ago

2210)             outfile = cast(
2211)                 TextIO,
2212)                 (
2213)                     export_settings
2214)                     if hasattr(export_settings, 'close')
2215)                     else click.open_file(os.fspath(export_settings), 'wt')
2216)                 ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 weeks ago

2224)                 if export_as == 'sh':
2225)                     this_ctx = ctx
2226)                     prog_name_pieces = collections.deque([
2227)                         this_ctx.info_name or 'vault',
2228)                     ])
2229)                     while (
2230)                         this_ctx.parent is not None
2231)                         and this_ctx.parent.info_name is not None
2232)                     ):
2233)                         prog_name_pieces.appendleft(this_ctx.parent.info_name)
2234)                         this_ctx = this_ctx.parent
2235)                     _print_config_as_sh_script(
2236)                         configuration,
2237)                         outfile=outfile,
2238)                         prog_name_list=prog_name_pieces,
2239)                     )
2240)                 else:
2241)                     json.dump(configuration, outfile)
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

2242)         except OSError as exc:
2243)             err('Cannot store config: %s: %r', exc.strerror, exc.filename)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

2251)         service_keys = {
2252)             'key',
2253)             'phrase',
2254)             'length',
2255)             'repeat',
2256)             'lower',
2257)             'upper',
2258)             'number',
2259)             'space',
2260)             'dash',
2261)             'symbol',
2262)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

2264)             {
2265)                 k: v
2266)                 for k, v in locals().items()
2267)                 if k in service_keys and v is not None
2268)             },
2269)             cast(
2270)                 dict[str, Any],
2271)                 configuration['services'].get(service or '', {}),
2272)             ),
2273)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2274)         )
2275)         if use_key:
2276)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

2277)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
2278)                     'ASCII'
2279)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

2284)             except NotImplementedError:
2285)                 err(
2286)                     'Cannot connect to SSH agent because '
2287)                     'this Python version does not support UNIX domain sockets'
2288)                 )
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

2289)             except OSError as exc:
2290)                 err('Cannot connect to SSH agent: %s', exc.strerror)
Marco Ricci Add a specific error class...

Marco Ricci authored 4 months ago

2291)             except (
2292)                 LookupError,
2293)                 RuntimeError,
2294)                 ssh_agent.SSHAgentFailedError,
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

2295)             ) as exc:
2296)                 err(str(exc))
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2297)         elif use_phrase:
2298)             maybe_phrase = _prompt_for_passphrase()
2299)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2301)             else:
2302)                 phrase = maybe_phrase
2303)         if store_config_only:
2304)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

2305)             view = (
2306)                 collections.ChainMap(*settings.maps[:2])
2307)                 if service
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

2310)             if use_key:
2311)                 view['key'] = key
2312)             elif use_phrase:
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 weeks ago

2315)                 try:
2316)                     _check_for_misleading_passphrase(
2317)                         ('services', service) if service else ('global',),
2318)                         {'phrase': phrase},
2319)                         main_config=user_config,
2320)                     )
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

2321)                 except AssertionError as exc:
2322)                     err('The configuration file is invalid.  ' + str(exc))
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 3 weeks ago

2324)                     if service:
2325)                         logger.warning(
2326)                             (
2327)                                 'Setting a service passphrase is ineffective '
2328)                                 'because a key is also set: %s'
2329)                             ),
2330)                             json.dumps(service),
2331)                         )
2332)                     else:
2333)                         logger.warning(
2334)                             'Setting a global passphrase is ineffective '
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

2341)                     f'actual settings'
2342)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 2 weeks ago

2344)             for setting in unset_settings:
2345)                 if setting in view.maps[0]:
2346)                     msg = (
2347)                         f'Attempted to unset and set --{setting} '
2348)                         f'at the same time.'
2349)                     )
2350)                     raise click.UsageError(msg)
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

2351)             subtree: dict[str, Any] = (
2352)                 configuration['services'].setdefault(service, {})  # type: ignore[assignment]
2353)                 if service
2354)                 else configuration.setdefault('global', {})
2355)             )
2356)             if overwrite_config:
2357)                 subtree.clear()
Marco Ricci Allow unsetting settings wh...

Marco Ricci authored 2 weeks ago

2358)             else:
2359)                 for setting in unset_settings:
2360)                     subtree.pop(setting, None)
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2366)         else:
2367)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

2370)             kwargs: dict[str, Any] = {
2371)                 k: v
2372)                 for k, v in settings.items()
2373)                 if k in service_keys and v is not None
2374)             }
2375) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 weeks ago

2377)                 try:
2378)                     _check_for_misleading_passphrase(
2379)                         _ORIGIN.INTERACTIVE,
2380)                         {'phrase': phrase},
2381)                         main_config=user_config,
2382)                     )
Marco Ricci Add small fixes to changelo...

Marco Ricci authored 2 weeks ago

2383)                 except AssertionError as exc:
2384)                     err('The configuration file is invalid.  ' + str(exc))
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 weeks ago

2393)                 kwargs['phrase'] = (
2394)                     _key_to_phrase(key, error_callback=err)
2395)                     if use_key
2396)                     else phrase
2397)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 weeks ago

2399)                 kwargs['phrase'] = _key_to_phrase(
2400)                     kwargs['key'], error_callback=err
2401)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

2402)             elif kwargs.get('phrase'):
2403)                 pass
2404)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

2407)                     'or in configuration'
2408)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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