From f957ad49b9843acc7f7aff1c0d9c0aaf9b9aa840 Mon Sep 17 00:00:00 2001 From: brent s Date: Fri, 3 Apr 2020 11:44:34 -0400 Subject: [PATCH] more argument parsing --- .gitignore | 1 + docs/README.adoc | 90 ++++++++++++++++++++++++++++- docs/gendocs.sh | 1 + pass.py | 17 ++++++ vaultpass/args.py | 128 ++++++++++++++++++++++++++++++++++++----- vaultpass/constants.py | 26 +++++++++ 6 files changed, 247 insertions(+), 16 deletions(-) create mode 100755 pass.py diff --git a/.gitignore b/.gitignore index f1425dc..b967518 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ testing/local.test.config.xml __pycache__/ logs/ docs/README.html +docs/vaultpass.1.gz diff --git a/docs/README.adoc b/docs/README.adoc index 5ebbe8c..ef79878 100644 --- a/docs/README.adoc +++ b/docs/README.adoc @@ -344,7 +344,6 @@ lCmbJtQcjxG/eJ/SrB2oS47YdEKRy+cH0Xx+ ---- - ===== Decrypted [source,xml] ---- @@ -356,3 +355,92 @@ lCmbJtQcjxG/eJ/SrB2oS47YdEKRy+cH0Xx+ ---- + + +== Known Incompatibilities with Pass +=== **`PASSWORD_STORE_ENABLE_EXTENSIONS`**,`.extensions/COMMAND.bash`, and Default Subcommands +==== Issue Description +Per the Pass man page: + +.PASS(1) +.... +If no COMMAND is specified, COMMAND defaults to either show or ls, depending on the type of specifier in ARGS. Alternatively, if PASSWORD_STORE_ENABLE_EXTENSIONS is set to "true", and the file .extensions/COMMAND.bash exists inside the password store and is executable, then it is sourced into the environment, passing any arguments and environment variables. Extensions existing in a system-wide directory, only installable by the administrator, are always enabled. +.... + +Due to this being Python, we lose some of this compatability. It may be possible to add this functionality in the +future, but it's lower priority currently. + +Similarly, we cannot set a default subcommand as of yet in Python via `argparse` (the library that VaultPass uses to +parse command-line arguments). + +==== Workaround(s) +You can set an alias in your `~/.bashrc` that will: + +. Execute `show` by default +. Provide a direct command for `ls` operations +. Specify default options for a command + +Via the following: + +.`~/.bashrc`: +[source,bash] +---- +# ... + +# 1 +alias pass='vaultpass show' + +# 2 +alias lpass='vaultpass ls' + +# 3 +alias vaultpass='vaultpass -c ~/.config/alternate.vaultpass.xml' +---- + +To use the non-aliased command in Bash, you can either invoke the full path: + +[source,bash] +---- +/usr/local/bin/vaultpass edit path/to/secret +---- + +Or, alternatively, prefix with a backslash: + +[source,bash] +---- +\vaultpass edit path/to/secret +---- + +Finally, you can always use VaultPass by specifying the subcommand and disregard aliases entirely. + + +=== **find**/**search** Subcommand Searching +==== Issue Description +Pass used http://man7.org/linux/man-pages/man1/find.1.html[**find(1)**^] to search secret paths. Because we use Vault +and not a filesystem hierarchy, this isn't applicable. As such, the normal https://www.gnu.org/software/findutils/manual/html_mono/find.html[`find`^] globbing language is not supported... + +==== Workaround(s) +What *is* supported, however, is regular expressions' ("regex") match patterns. + +If you haven't used regexes before, here are some helpful starters/tools: + +* https://www.regular-expressions.info/tutorial.html +* https://regexone.com/ +* https://regexr.com/ +* https://docs.python.org/library/re.html#regular-expression-syntax +* https://regexcrossword.com/ +* https://learncodethehardway.org/regex/ + +Regular expressions are MUCH more powerful than the `find` globbing language, but do have a slight learning curve. You +will be thankful to learn their syntax, however, as they are very widely applicable. + +=== Environment Variables +==== Issue Description +Pass (and to a slightly lesser extent, Vault) relies almost entirely/exclusively upon environment variables for +configuration. VaultPass does not. + +==== Workaround(s) +Relying entirely on environment variables for configuration is dumb, so I don't rely on that. All persistent +configuration can be either specified in the <> or can be overridden by +flags/switches to subcommands. **Some** configuration directives/behaviour may be overridden by environment variables, +but by and large this is not the case. diff --git a/docs/gendocs.sh b/docs/gendocs.sh index 8c82745..b55974a 100755 --- a/docs/gendocs.sh +++ b/docs/gendocs.sh @@ -1,3 +1,4 @@ #!/bin/bash asciidoctor README.adoc -o ./README.html +asciidoctor -b manpage -o - vaultpass.1.adoc | gzip -c -9 > vaultpass.1.gz diff --git a/pass.py b/pass.py new file mode 100755 index 0000000..b3d5835 --- /dev/null +++ b/pass.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import vaultpass + + +def main(): + rawargs = vaultpass.args.parseArgs() + args = rawargs.parse_args() + if not args.oper: + args.oper = 'show' + import pprint + pprint.pprint(vars(args)) + return(None) + + +if __name__ == '__main__': + main() diff --git a/vaultpass/args.py b/vaultpass/args.py index e169bf7..7a71f2c 100644 --- a/vaultpass/args.py +++ b/vaultpass/args.py @@ -1,21 +1,119 @@ import argparse - - -_opers = ['cp', 'edit', 'find', 'generate', 'git', 'grep', 'help', 'init', 'insert', 'ls', 'mv', 'rm', 'show', - 'version', 'import'] # "import" is new +## +from . import constants def parseArgs(): args = argparse.ArgumentParser(description = 'VaultPass - a Vault-backed Pass replacement', prog = 'pass', - epilog = ('This program has context-specific help. Try ')) - commonargs = argparse.ArgumentParser(add_help = False) - commonargs.add_argument('-c', '--config', - default = '~/.config/vaultpass.xml', - help = ('The path to your configuration file. Default: ~/.config/vaultpass.xml')) - - args.add_argument('oper', - choices = _opers, - help = ('The operation to perform. Use the help operation or see the man page for more ' - 'information')) - args.add_argument() + epilog = ('This program has context-specific help. Try "... cp --help". ' + 'This help output is intentionally terse; see "man 1 vaultpass" and the ' + 'README for more complete information, configuration, and usage.')) + args.add_argument('-c', '--config', + default = '~/.config/vaultpass.xml', + help = ('The path to your configuration file. Default: ~/.config/vaultpass.xml')) + args.add_argument('-m', '--mount', + dest = 'mount', + required = False, + help = ('The mount to use in OPERATION. If not specified, assume all mounts we have access ' + 'to/all mounts specified in -c/--config')) + # I wish argparse supported default subcommands. It doesn't as of python 3.8. + subparser = args.add_subparsers(help = ('Operation to perform'), + metavar = 'OPERATION', + dest = 'oper') + cp = subparser.add_parser('cp', + aliases = ['copy']) + edit = subparser.add_parser('edit') + find = subparser.add_parser('find', + aliases = ['search']) + gen = subparser.add_parser('generate') + git = subparser.add_parser('git') # Dummy opt; do nothing + grep = subparser.add_parser('grep') + helpme = subparser.add_parser('help') + initvault = subparser.add_parser('init') + insertval = subparser.add_parser('insert', + aliases = ['add']) + ls = subparser.add_parser('ls', + aliases = ['list']) + mv = subparser.add_parser('mv', + aliases = ['rename']) + rm = subparser.add_parser('rm', + aliases = ['remove', 'delete']) + show = subparser.add_parser('show') + version = subparser.add_parser('version') + importvault = subparser.add_parser('import') + # CP/COPY + cp.add_argument('-f', '--force', + dest = 'force', + action = 'store_true', + help = ('If specified, replace NEWPATH if it exists')) + cp.add_argument('oldpath', + metavar = 'OLDPATH', + help = ('The original ("source") path for the secret')) + cp.add_argument('newpath', + metavar = 'NEWPATH', + help = ('The new ("destination") path for the secret')) + # EDIT + edit.add_argument('-e', '--editor', + metavar = '/PATH/TO/EDITOR', + dest = 'editor', + default = constants.EDITOR, + help = ('The editor program to use (sourced from EDITOR environment variable). ' + 'Default: {0}').format(constants.EDITOR)) + edit.add_argument('path', + metavar = 'PATH_TO_SECRET', + help = ('Insert a new secret at PATH_TO_SECRET if it does not exist, otherwise edit it using ' + 'your default editor (see -e/--editor)')) + # FIND/SEARCH + find.add_argument('pattern', + metavar = 'NAME_PATTERN', + help = ('List secrets\' paths whose names match the regex NAME_PATTERN')) + # GENERATE + gen.add_argument('-n', '--no-symbols', + dest = 'symbols', + action = 'store_false', + help = ('If specified, generate a password with no non-alphanumeric chracters')) + gen.add_argument('-c', '--clip', + dest = 'clip', + action = 'store_true', + help = ('If specified, do not print the password but instead place in the clipboard for ' + 'a given number of seconds (see -s/--seconds)')) + gen.add_argument('-s', '--seconds', + dest = 'seconds', + type = int, + default = constants.CLIP_TIMEOUT, + help = ('If generating to the clipboard (see -c/--clip), clear the clipboard after this many ' + 'seconds. Default: {0}').format(constants.CLIP_TIMEOUT)) + gen.add_argument('-C', '--characters', + dest = 'chars', + default = constants.SELECTED_PASS_CHARS, + help = ('The characters to use when generating a password (symbols included). ' + 'Default: {0}').format(constants.SELECTED_PASS_CHARS)) + gen.add_argument('-Cn', '--characters-no-symbols', + dest = 'chars_plain', + default = constants.SELECTED_PASS_NOSYMBOL_CHARS, + help = ('The characters to use when generating an alphanumeric-only password, ' + 'Default: {0}').format(constants.SELECTED_PASS_NOSYMBOL_CHARS)) + # TODO: support? + gen.add_argument('-i', '--in-place', + dest = 'in_place', + action = 'store_true', + help = ('(Unused; kept for compatibility reasons)')) + gen.add_argument('-q', '--qrcode', + dest = 'qr', + action = 'store_true', + help = ('If specified, display the password as a QR code (graphically or in-terminal depending ' + 'on supported environment)')) + gen.add_argument('-f', '--force', + dest = 'force', + help = ('If specified and PATH/TO/SECRET exists, overwrite without prompting first')) + gen.add_argument('path', + metavar = 'PATH/TO/SECRET', + help = ('The path to the secret')) + gen.add_argument('length', + type = int, + default = constants.GENERATED_LENGTH, + metavar = 'LENGTH', + help = ('The length (number of characters) in the generated password. ' + 'Default: {0}').format(constants.GENERATED_LENGTH)) + return(args) diff --git a/vaultpass/constants.py b/vaultpass/constants.py index a4e55ec..33905c8 100644 --- a/vaultpass/constants.py +++ b/vaultpass/constants.py @@ -1 +1,27 @@ +import os +import string + +# These are static. VERSION = '0.0.1' +ALPHA_PASS_CHARS = string.ascii_letters +NUM_PASS_CHARS = string.digits +ALPHANUM_PASS_CHARS = ALPHA_PASS_CHARS + NUM_PASS_CHARS +SYMBOL_PASS_CHARS = string.punctuation +ALL_PASS_CHARS = ALPHANUM_PASS_CHARS + SYMBOL_PASS_CHARS +# These CAN be generated dynamically, see below. +CLIP_TIMEOUT = 45 +SELECTED_PASS_CHARS = ALL_PASS_CHARS +SELECTED_PASS_NOSYMBOL_CHARS = ALPHANUM_PASS_CHARS +CLIPBOARD = 'clipboard' +GENERATED_LENGTH = 25 # I personally would prefer 32, but Pass compatability... +EDITOR = 'vi' # vi is on ...every? single distro and UNIX/UNIX-like, to my knowledge. + +if not os.environ.get('NO_VAULTPASS_ENVS'): + # These are dynamically generated from the environment. + CLIP_TIMEOUT = int(os.environ.get('PASSWORD_STORE_CLIP_TIME', CLIP_TIMEOUT)) + SELECTED_PASS_CHARS = os.environ.get('PASSWORD_STORE_CHARACTER_SET', SELECTED_PASS_CHARS) + SELECTED_PASS_NOSYMBOL_CHARS = os.environ.get('PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS', + SELECTED_PASS_NOSYMBOL_CHARS) + CLIPBOARD = os.environ.get('PASSWORD_STORE_X_SELECTION', CLIPBOARD) + GENERATED_LENGTH = int(os.environ.get('PASSWORD_STORE_GENERATED_LENGTH', GENERATED_LENGTH)) + EDITOR = os.environ.get('EDITOR', EDITOR)