args' corresponding functions spec'd out and found (and have workaround for) racetime condition in Vault.

This commit is contained in:
brent s. 2020-04-05 03:26:52 -04:00
parent a3b370cc6e
commit 439e86d8c3
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
7 changed files with 228 additions and 35 deletions

View File

@ -15,6 +15,7 @@ default_conf = {'listener': [
'storage': {'file': {'path': './data'}},
'log_level': 'Debug', # highest is 'Trace'
'pid_file': './test.pid',
'disable_mlock': True,
'raw_storage_endpoint': True,
'log_format': 'json', # or String
'ui': True}

View File

@ -217,6 +217,9 @@ class VaultSpawner(object):
return(None)

def cleanup(self):
self._connCheck(bind = False)
if self.is_running:
return(None)
storage = self.conf.get('storage', {}).get('file', {}).get('path')
if not storage:
return(None)
@ -249,7 +252,7 @@ class VaultSpawner(object):
mounts['secret'] = 'kv2'
mounts['secret_legacy'] = 'kv1'
for idx, (mname, mtype) in enumerate(mounts.items()):
opts = None
opts = {}
orig_mtype = mtype
if mtype.startswith('kv'):
opts = {'version': re.sub(r'^kv([0-9]+)$', r'\g<1>', mtype)}
@ -259,9 +262,12 @@ class VaultSpawner(object):
path = mname,
description = 'Testing mount ({0})'.format(mtype),
options = opts)
except hvac.exceptions.InvalidRequest:
# We might have some issues writing secrets on fast machines.
time.sleep(2)
except hvac.exceptions.InvalidRequest as e:
# It probably already exists.
pass
print('Exception creating {0}: {1} ({2})'.format(mname, e, e.__class__))
print(opts)
if orig_mtype not in ('kv1', 'kv2', 'cubbyhole'):
continue
args = {'path': 'test_secret{0}/foo{1}'.format(idx, mname),
@ -279,11 +285,12 @@ class VaultSpawner(object):
elif orig_mtype == 'kv2':
handler = self.client.secrets.kv.v2.create_or_update_secret
try:
handler(**args)
resp = handler(**args)
except hvac.exceptions.InvalidPath:
print('{0} path invalid'.format(args['path']))
except Exception as e:
print('Exception: {0} ({1})'.format(e, e.__class__))
print('Exception creating {0} on {1}: {2} ({3})'.format(args['path'], args['mount_point'], e, e.__class__))
print(args)
return(None)

def start(self):
@ -324,6 +331,7 @@ class VaultSpawner(object):
if self.cmd:
self.cmd.kill()
else:
if self.pid:
import signal
os.kill(self.pid, signal.SIGKILL)
return(None)
@ -337,7 +345,7 @@ def parseArgs():
help = ('If specified, do not populate with test data (if it doesn\'t exist)'))
args.add_argument('-d', '--delete',
dest = 'delete_storage',
action = 'store_false',
action = 'store_true',
help = ('If specified, delete the storage backend first so a fresh instance is created'))
args.add_argument('-c', '--cleanup',
dest = 'cleanup',
@ -369,6 +377,7 @@ def main():
elif args.oper == 'stop':
s.stop()
if args.cleanup:
time.sleep(2)
s.cleanup()
return(None)


View File

@ -8,11 +8,12 @@ from . import auth
from . import clipboard
from . import config
from . import constants
from . import gpg_handler
from . import mounts
from . import pass_import


class PassMan(object):
class VaultPass(object):
client = None
auth = None
uri = None
@ -64,6 +65,61 @@ class PassMan(object):
_logger.debug('Set URI to {0}'.format(self.uri))
return(None)

def convert(self,
mount,
force = False,
gpghome = constants.GPG_HOMEDIR,
pass_dir = constants.PASS_DIR,
*args, **kwargs):
pass # TODO

def copySecret(self, oldpath, newpath, mount, newmount, force = False, remove_old = False, *args, **kwargs):
pass # TODO

if remove_old:
self.deleteSecret(oldpath, mount, force = force)
return(None)

def createSecret(self, secret_dict, path, mount_name, *args, **kwargs):
mtype = self.mount.mounts.get(mount_name)
handler = None
if not mtype:
_logger.error('Could not determine mount type')
_logger.debug('Could not determine mount type for mount {0}'.format(mount_name))
raise RuntimeError('Could not determine mount type')
args = {'path': path,
'mount_point': mount_name,
'secret': secret_dict}
if mtype == 'cubbyhole':
handler = self.mount.cubbyhandler.write_secret
elif mtype == 'kv1':
handler = self.client.secrets.kv.v1.create_or_update_secret
elif mtype == 'kv2':
handler = self.client.secrets.kv.v2.create_or_update_secret
resp = handler(**args)
return(resp)

def deleteSecret(self, path, mount_name, force = False, recursive = False, *args, **kwargs):
pass # TODO

def editSecret(self, path, mount, editor = constants.EDITOR, *args, **kwargs):
pass # TODO

def generateSecret(self,
path,
mount,
symbols = True,
clip = False,
seconds = constants.CLIP_TIMEOUT,
chars = constants.SELECTED_PASS_CHARS,
chars_plain = constants.SELECTED_PASS_NOSYMBOL_CHARS,
in_place = False,
qr = False,
force = False,
length = constants.GENERATED_LENGTH,
*args, **kwargs):
pass # TODO

def getClient(self):
auth_xml = self.cfg.xml.find('.//auth')
if auth_xml is None:
@ -95,3 +151,28 @@ class PassMan(object):
_logger.error('Not initialized')
raise RuntimeError('Not initialized')
return(None)

def getSecret(self, path, mount, clip = None, qr = None, seconds = constants.CLIP_TIMEOUT, *args, **kwargs):
pass # TODO

def initVault(self, *args, **kwargs):
pass # TODO

def insertSecret(self,
path,
mount,
allow_shouldersurf = False,
multiline = False,
force = False,
confirm = True,
*args, **kwargs):
pass # TODO

def listSecretNames(self, path, mount, output = None, indent = 4, *args, **kwargs):
pass # TODO

def searchSecrets(self, pattern, mount, *args, **kwargs):
pass # TODO

def searchSecretNames(self, pattern, mount, *args, **kwargs):
pass # TODO

View File

@ -18,9 +18,8 @@ def parseArgs():
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'))
default = 'secret',
help = ('The mount to use in OPERATION. If not specified, assume a mount named "secret"'))
# I wish argparse supported default subcommands. It doesn't as of python 3.8.
subparser = args.add_subparsers(help = ('Operation to perform'),
metavar = 'OPERATION',
@ -78,10 +77,16 @@ def parseArgs():
description = ('Import your existing Pass into Vault'),
help = ('Import your existing Pass into Vault'))
# CP/COPY
# vp.copySecret()
cp.add_argument('-f', '--force',
dest = 'force',
action = 'store_true',
help = ('If specified, replace NEWPATH if it exists'))
cp.add_argument('-m', '--mount',
dest = 'newmount',
nargs = 1,
required = False,
help = ('The mount for the destination. Default is to use the main command\'s -m/--mount'))
cp.add_argument('oldpath',
metavar = 'OLDPATH',
help = ('The original ("source") path for the secret'))
@ -89,6 +94,7 @@ def parseArgs():
metavar = 'NEWPATH',
help = ('The new ("destination") path for the secret'))
# EDIT
# vp.editSecret()
edit.add_argument('-e', '--editor',
metavar = '/PATH/TO/EDITOR',
dest = 'editor',
@ -100,10 +106,12 @@ def parseArgs():
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
# vp.searchSecretNames()
find.add_argument('pattern',
metavar = 'NAME_PATTERN',
help = ('List secrets\' paths whose names match the regex NAME_PATTERN'))
# GENERATE
# vp.generateSecret()
# TODO: feature parity with passgen (spaces? etc.)
gen.add_argument('-n', '--no-symbols',
dest = 'symbols',
@ -160,6 +168,7 @@ def parseArgs():
metavar = 'dummy',
help = ('(Unused; kept for compatibility reasons)'))
# GREP
# vp.searchSecrets()
# I wish argparse supported arbitrary arguments.
# It *KIND* of does: https://stackoverflow.com/a/37367814/733214 but then I wouldn't be able to properly grab the
# regex pattern without more hackery. So here's to wasting my life.
@ -323,6 +332,7 @@ def parseArgs():
help = ('Regex pattern to search passwords'))
# HELP has no arguments.
# INIT
# vp.initVault()
initvault.add_argument('-p', '--path',
dest = 'path',
help = ('(Dummy option; kept for compatibility reasons)'))
@ -330,6 +340,7 @@ def parseArgs():
dest = 'gpg_id',
help = ('(Dummy option; kept for compatibility reasons)'))
# INSERT
# vp.insertSecret()
# TODO: if -e/--echo is specified and sys.stdin, use sys.stdin rather than prompt
insertval.add_argument('-e', '--echo',
dest = 'allow_shouldersurf',
@ -348,11 +359,30 @@ def parseArgs():
action = 'store_false',
help = ('If specified, disable password prompt confirmation. '
'Has no effect if -e/--echo is specified'))
insertval.add_argument('path',
metavar = 'PATH/TO/SECRET',
help = ('The path to the secret'))
# LS
# vp.listSecretNames()/vp.mount.print() ?
ls.add_argument('-o', '--output',
dest = 'output',
choices = constants.SUPPORTED_OUTPUT_FORMATS,
metavar = 'OUTPUT_FORMAT',
help = ('The format to output the hierarchy in. '
'If specified, must be one of: {0} '
'(the default is a condensed python '
'dict repr)').format(', '.join(constants.SUPPORTED_OUTPUT_FORMATS)))
ls.add_argument('-i', '--indent',
type = int,
default = 4,
dest = 'indent',
help = ('If -o/--output is "pretty", "yaml", or "json", specify the indent level. '
'Default is 4'))
ls.add_argument('path',
metavar = 'PATH/TO/TREE/BASE',
help = ('List names of secrets recursively, starting at PATH/TO/TREE/BASE'))
# MV
# vp.copySecret(remove_old = True)
mv.add_argument('-f', '--force',
dest = 'force',
action = 'store_true',
@ -364,6 +394,7 @@ def parseArgs():
metavar = 'NEWPATH',
help = ('The new ("destination") path for the secret'))
# RM
# vp.deleteSecret()
# Is this argument even sensible since it isn't a filesystem?
rm.add_argument('-r', '--recursive',
dest = 'recurse',
@ -377,6 +408,8 @@ def parseArgs():
metavar = 'PATH/TO/SECRET',
help = ('The path to the secret or subdirectory'))
# SHOW
# vp.getSecret() ? plus QR etc. printing
# TODO: does the default overwrite the None if not specified?
show.add_argument('-c', '--clip',
nargs = '?',
type = int,
@ -387,6 +420,7 @@ def parseArgs():
'clipboard instead of printing it. '
'Use 0 for LINE_NUMBER for the entire secret').format(constants.SHOW_CLIP_LINENUM))
show.add_argument('-q', '--qrcode',
dest = 'qr',
nargs = '?',
type = int,
metavar = 'LINE_NUMBER',
@ -406,26 +440,18 @@ def parseArgs():
help = ('The path to the secret'))
# VERSION has no args.
# IMPORT
def_pass_dir = os.path.abspath(os.path.expanduser(os.environ.get('PASSWORD_STORE_DIR', '~/.password-store')))
def_gpg_dir = os.path.abspath(os.path.expanduser(constants.SELECTED_GPG_HOMEDIR))
# vp.convert()
importvault.add_argument('-d', '--directory',
default = def_pass_dir,
default = constants.PASS_DIR,
metavar = '/PATH/TO/PASSWORD_STORE/DIR',
dest = 'pass_dir',
help = ('The path to your Pass data directory. Default: {0}').format(def_pass_dir))
importvault.add_argument('-k', '--gpg-key-id',
metavar = 'KEY_ID',
dest = 'key_id',
default = constants.PASS_KEY,
help = ('The GPG key ID to use. Default: {0}. '
'(If None, the default is to first check /PATH/TO/PASSWORD_STORE/DIR/.gpg-id if '
'it exists, otherwise use the '
'first private key we find)').format(constants.PASS_KEY))
help = ('The path to your Pass data directory. Default: {0}').format(constants.PASS_DIR))
importvault.add_argument('-H', '--gpg-homedir',
default = def_gpg_dir,
default = constants.GPG_HOMEDIR,
dest = 'gpghome',
metavar = '/PATH/TO/GNUPG/HOMEDIR',
help = ('The GnuPG "homedir". Default: {0}').format(def_gpg_dir))
help = ('The GnuPG "homedir". It MUST contain the private key that Pass uses. '
'Default: {0}').format(constants.GPG_HOMEDIR))
importvault.add_argument('-f', '--force',
dest = 'force',
action = 'store_true',

View File

@ -4,6 +4,8 @@ import string
# These are static.
NAME = 'VaultPass'
VERSION = '0.0.1'
SUPPORTED_ENGINES = ('kv1', 'kv2', 'cubbyhole')
SUPPORTED_OUTPUT_FORMATS = ('pretty', 'yaml', 'json')
ALPHA_LOWER_PASS_CHARS = string.ascii_lowercase
ALPHA_UPPER_PASS_CHARS = string.ascii_uppercase
ALPHA_PASS_CHARS = ALPHA_LOWER_PASS_CHARS + ALPHA_UPPER_PASS_CHARS
@ -19,9 +21,9 @@ SELECTED_PASS_NOSYMBOL_CHARS = ALPHANUM_PASS_CHARS
CLIPBOARD = 'clipboard'
GENERATED_LENGTH = 25 # I personally would prefer 32, but Pass compatibility...
EDITOR = 'vi' # vi is on ...every? single distro and UNIX/UNIX-like, to my knowledge.
PASS_KEY = None
GPG_HOMEDIR = '~/.gnupg'
SELECTED_GPG_HOMEDIR = GPG_HOMEDIR
PASS_DIR = '~/.password-store'

if not os.environ.get('NO_VAULTPASS_ENVS'):
# These are dynamically generated from the environment.
@ -32,5 +34,9 @@ if not os.environ.get('NO_VAULTPASS_ENVS'):
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)
PASS_KEY = os.environ.get('PASSWORD_STORE_KEY', PASS_KEY)
SELECTED_GPG_HOMEDIR = os.environ.get('GNUPGHOME', GPG_HOMEDIR)
PASS_DIR = os.environ.get('PASSWORD_STORE_DIR', PASS_DIR)

# These are made more sane.
PASS_DIR = os.path.abspath(os.path.expanduser(PASS_DIR))
SELECTED_GPG_HOMEDIR = os.path.abspath(os.path.expanduser(SELECTED_GPG_HOMEDIR))

View File

@ -18,8 +18,17 @@ class GPG(object):

def decrypt(self, fpath):
fpath = os.path.abspath(os.path.expanduser(fpath))
_logger.debug('Opening {0} for decryption'.format(fpath))
with open(fpath, 'rb') as fh:
iobuf = io.BytesIO(fh.read())
data = fh.read()
decrypted = self.decryptData(data)
return(decrypted)

def decryptData(self, data):
if isinstance(data, str):
data = data.encode('utf-8')
_logger.debug('Decrypting {0} bytes'.format(len(data)))
iobuf = io.BytesIO(data)
iobuf.seek(0, 0)
rslt = self.gpg.decrypt(iobuf)
decrypted = rslt[0]

View File

@ -1,15 +1,23 @@
import copy
import logging
import re
import shutil
import time
import warnings
##
import dpath.util # https://pypi.org/project/dpath/
import hvac.exceptions
##
from . import constants


_logger = logging.getLogger()
_mount_re = re.compile(r'^(?P<mount>.*)/$')
_subpath_re = re.compile(r'^/?(?P<path>.*)/$')
_kv_re = re.compile(r'^kv(?:-v)?(?P<version>[0-9]+)$')


# TODO: for all write operations, modify handler call to first check if path exists and patch if it does?


class CubbyHandler(object):
@ -30,6 +38,18 @@ class CubbyHandler(object):
resp = self.client._adapter.get(url = uri)
return(resp.json())

def write_secret(self, path, secret, mount_point = 'cubbyhole'):
path = path.lstrip('/')
args = {'path': '/'.join((mount_point, path))}
for k, v in secret.items():
if k in args.keys():
_logger.error('Cannot use reserved secret name')
_logger.debug('Cannot use secret name {0} as it is reserved'.format(k))
raise ValueError('Cannot use reserved secret name')
args[k] = v
resp = self.client.write(**args)
return(resp)


class MountHandler(object):
internal_mounts = ('identity', 'sys')
@ -42,6 +62,38 @@ class MountHandler(object):
self.paths = {}
self.getSysMounts()

def createMount(self, mount_name, mount_type = 'kv2'):
orig_mtype = mount_type
if mount_type not in constants.SUPPORTED_ENGINES:
_logger.error('Invalid mount type')
_logger.debug(('The mount type {0} is invalid. '
'It must be one of: {1}').format(mount_type, ', '.join(constants.SUPPORTED_ENGINES)))
raise ValueError('Invalid mount type')
options = {}
r = _kv_re.search(mount_type)
if r:
mount_type = 'kv'
options['version'] = r.groupdict()['version']
created = False
try:
self.client.sys.enable_secrets_engine(mount_type,
path = mount_name,
description = 'Created automatically by VaultPass',
options = options)
created = True
except hvac.exceptions.InvalidPath as e:
_logger.error('Invalid path')
_logger.debug('The mount path {0} (type {1}) is invalid: {2}'.format(mount_name, orig_mtype, e))
raise ValueError('Invalid path')
except hvac.exceptions.InvalidRequest as e:
_logger.error('Invalid request; does mount already exist?')
_logger.debug(('The creation of mount path {0} (type {1}) generated an invalid request: '
'{2}. Does it already exist?').format(mount_name, orig_mtype, e))
# Due to how KV2 is created, we can hit a timing/race condition.
if orig_mtype == 'kv2' and created:
time.sleep(2)
return(created)

def getMountType(self, mount):
if not self.mounts:
self.getSysMounts()
@ -53,7 +105,19 @@ class MountHandler(object):
return(mtype)

def getSecret(self, path, mount, version = None):
pass
if not self.mounts:
self.getSysMounts()
mtype = self.getMountType(mount)
args = {}
handler = None
if mtype == 'cubbyhole':
handler = self.cubbyhandler.read_secret
elif mtype == 'kv1':
handler = self.client.secrets.kv.v1.read_secret
if mtype == 'kv2':
args['version'] = version
data = self.client.secrets
# TODO

def getSecretNames(self, path, mount, version = None):
reader = None
@ -214,6 +278,3 @@ class MountHandler(object):
elif not output:
return(str(self.paths))
return(None)

def search(self):
pass