more argument parsing

This commit is contained in:
brent s. 2020-04-03 11:44:34 -04:00
parent bc755a8068
commit f957ad49b9
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
6 changed files with 247 additions and 16 deletions

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ testing/local.test.config.xml
__pycache__/
logs/
docs/README.html
docs/vaultpass.1.gz

View File

@ -344,7 +344,6 @@ lCmbJtQcjxG/eJ/SrB2oS47YdEKRy+cH0Xx+
<BINARY DATA>
----


===== Decrypted
[source,xml]
----
@ -356,3 +355,92 @@ lCmbJtQcjxG/eJ/SrB2oS47YdEKRy+cH0Xx+

</auth>
----


== 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 <<configuration,configuration file>> 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.

View File

@ -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

17
pass.py Executable file
View File

@ -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()

View File

@ -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)

View File

@ -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)