finishing up some of the mount parsing. it now successfully builds a dictionary map at least. docs updated and print formatter done.
This commit is contained in:
parent
58accf8c7e
commit
236af1ea37
@ -58,6 +58,7 @@ One of either:footnote:optelem[]
|
||||
one of either:
|
||||
.... `auth` (see <<Auth>> section below), or
|
||||
.... `authGpg`, an <<Auth>> config snippet encrypted with GPG. See the section on <<GPG-Encrypted Elements>>.
|
||||
... An optional `mounts` container.footnote:optelem[] See the section on <<Mounts>>.
|
||||
|
||||
Let's look at an example configuration.
|
||||
|
||||
@ -78,6 +79,11 @@ Let's look at an example configuration.
|
||||
<auth>
|
||||
<token/>
|
||||
</auth>
|
||||
<mounts>
|
||||
<mount type="kv1">secret_legacy</mount>
|
||||
<mount type="kv2">secret</mount>
|
||||
<mount type="cubbyhole">cubbyhole</mount>
|
||||
</mounts>
|
||||
|
||||
</vaultpass>
|
||||
----
|
||||
@ -85,7 +91,7 @@ Let's look at an example configuration.
|
||||
In the above, we can see that it would use the vault server at `http://localhost:8200/` using whatever token is either
|
||||
in the **`VAULT_TOKEN`** environment variable or, if empty, the `~/.vault-token` file. Because an unseal shard was
|
||||
provided, it will be able to attempt to automatically unseal the Vault (assuming its shard will complete the threshold
|
||||
needed).
|
||||
needed). Because we specify mounts, we do not need permissions in Vault to list `/sys/mounts`.
|
||||
|
||||
=== Auth
|
||||
Vault itself supports a https://www.vaultproject.io/docs/auth/[large number of authentication methods^]. However, in
|
||||
@ -221,6 +227,25 @@ If not specified, the default is `userpass`.
|
||||
<!-- SNIP -->
|
||||
----
|
||||
|
||||
=== Mounts
|
||||
VaultPass has the ability to automatically detect (some) mounts and their paths.
|
||||
|
||||
So why, then, should you specify them in the configuration file? Simple: because you might not have permission to list
|
||||
them! Even if you can see the mounts in the web UI that you have permission to, that **doesn't guarantee** that they're
|
||||
accessible/viewable https://www.vaultproject.io/api-docs/[via the API^] (which is how VaultPass, and even the upstream
|
||||
Vault binary client, operates). So by specifying them in the configuration file, you're able to "bootstrap" the process.
|
||||
|
||||
The optional `mounts` footnote:optelem[] container contains one or more `mount` child elements, with the name of the
|
||||
mountpoint as the content.
|
||||
|
||||
Each `mount` element has one optional attribute, `type` footnote:optelem[], which can be one of:
|
||||
|
||||
* https://www.vaultproject.io/docs/secrets/cubbyhole/[`cubbyhole`^]
|
||||
* https://www.vaultproject.io/docs/secrets/kv/kv-v1/[`kv1`^]
|
||||
* https://www.vaultproject.io/docs/secrets/kv/kv-v2/[`kv2`^] _(this is the default if not specified)_
|
||||
|
||||
https://www.vaultproject.io/docs/secrets/[More mount types^] may be added upon popular demand and technical feasability.
|
||||
|
||||
=== GPG-Encrypted Elements
|
||||
Understandably, in order to have a persistent configuration, that means storing on disk. That also means that they need
|
||||
to be able to be accessed with no or minimal user interruption. Pass used GPG natively, so it didn't have an issue with
|
||||
@ -257,7 +282,7 @@ Let's look at an example of GPG-encrypted elements.
|
||||
|
||||
==== GPG-Encrypted Elements Example
|
||||
|
||||
.`~/.config/vaultpass.xml` snippet:
|
||||
.`~/.config/vaultpass.xml`:
|
||||
[source,xml]
|
||||
----
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
@ -14,8 +14,8 @@
|
||||
be able to iterate through available mounts if not. -->
|
||||
<!-- Default type if not specified is kv2. -->
|
||||
<mounts>
|
||||
<mount type="kv">secrets_legacy</mount>
|
||||
<mount type="kv2">secrets</mount>
|
||||
<mount type="kv1">secret_legacy</mount>
|
||||
<mount type="kv2">secret</mount>
|
||||
<mount type="cubbyhole">cubbyhole</mount>
|
||||
</mounts>
|
||||
</vaultpass>
|
||||
|
@ -41,7 +41,7 @@ class PassMan(object):
|
||||
return(None)
|
||||
|
||||
def _getMount(self):
|
||||
mounts_xml = self.xml.find('.//mounts')
|
||||
mounts_xml = self.cfg.xml.find('.//mounts')
|
||||
self.mount = mounts.MountHandler(self.client, mounts_xml = mounts_xml)
|
||||
return(None)
|
||||
|
||||
|
@ -2,7 +2,7 @@ import logging
|
||||
import re
|
||||
import warnings
|
||||
##
|
||||
import dpath # https://pypi.org/project/dpath/
|
||||
import dpath.util # https://pypi.org/project/dpath/
|
||||
import hvac.exceptions
|
||||
|
||||
|
||||
@ -23,10 +23,11 @@ class CubbyHandler(object):
|
||||
resp = self.client._adapter.list(url = uri)
|
||||
return(resp.json())
|
||||
|
||||
def read_secret(self, *args, **kwargs):
|
||||
# https://github.com/hashicorp/vault/issues/8644
|
||||
_logger.warning('Cannot get path info from a cubbyhole')
|
||||
return({'data': {}})
|
||||
def read_secret(self, path, mount_point = 'cubbyhole'):
|
||||
path = path.lstrip('/')
|
||||
uri = '/v1/{0}/{1}'.format(mount_point, path)
|
||||
resp = self.client._adapter.get(url = uri)
|
||||
return(resp.json())
|
||||
|
||||
|
||||
class MountHandler(object):
|
||||
@ -40,6 +41,105 @@ class MountHandler(object):
|
||||
self.paths = {}
|
||||
self.getSysMounts()
|
||||
|
||||
def getMountType(self, mount):
|
||||
if not self.mounts:
|
||||
self.getSysMounts()
|
||||
mtype = self.mounts.get(mount)
|
||||
if not mtype:
|
||||
_logger.error('Mount not found in defined mounts')
|
||||
_logger.debug('The mount {0} was not found in the defined mounts.'.format(mount))
|
||||
raise ValueError('Mount not found in defined mounts')
|
||||
return(mtype)
|
||||
|
||||
def getSecret(self, path, mount, version = None):
|
||||
pass
|
||||
|
||||
def getSecretNames(self, path, mount, version = None):
|
||||
reader = None
|
||||
mtype = self.getMountType(mount)
|
||||
secrets_list = []
|
||||
keypath = ['data']
|
||||
args = {'path': path,
|
||||
'mount_point': mount}
|
||||
if mtype == 'cubbyhole':
|
||||
reader = self.cubbyhandler.read_secret
|
||||
elif mtype == 'kv1':
|
||||
reader = self.client.secrets.kv.v1.read_secret
|
||||
elif mtype == 'kv2':
|
||||
if not any(((version is None), isinstance(version, int))):
|
||||
_logger.error('version parameter must be an integer or None')
|
||||
_logger.debug('The version parameter ({0}) must be an integer or None'.format(version))
|
||||
raise ValueError('version parameter must be an integer or None')
|
||||
reader = self.client.secrets.kv.v2.read_secret_version
|
||||
args['version'] = version
|
||||
keypath = ['data', 'data']
|
||||
data = reader(**args)
|
||||
try:
|
||||
# secrets_list = list(data.get('data', {}).keys())
|
||||
secrets_list = list(dpath.util.get(data, keypath, {}).keys())
|
||||
except (KeyError, TypeError):
|
||||
secrets_list = []
|
||||
return(secrets_list)
|
||||
|
||||
def getSecretsTree(self, path = '/', mounts = None, version = None):
|
||||
if not mounts:
|
||||
mounts = self.mounts
|
||||
if isinstance(mounts, dict):
|
||||
mounts = list(mounts.keys())
|
||||
if not isinstance(mounts, list):
|
||||
mounts = [mounts]
|
||||
for mount in mounts:
|
||||
mtype = self.getMountType(mount)
|
||||
handler = None
|
||||
args = {'path': path,
|
||||
'mount_point': mount}
|
||||
relpath = path.replace('//', '/').lstrip('/')
|
||||
fullpath = '/'.join((mount, relpath)).replace('//', '/').lstrip('/')
|
||||
if mtype == 'cubbyhole':
|
||||
handler = self.cubbyhandler
|
||||
elif mtype == 'kv1':
|
||||
handler = self.client.secrets.kv.v1
|
||||
elif mtype == 'kv2':
|
||||
if not any(((version is None), isinstance(version, int))):
|
||||
_logger.error('version parameter must be an integer or None')
|
||||
_logger.debug('The version parameter ({0}) must be an integer or None'.format(version))
|
||||
raise ValueError('version parameter must be an integer or None')
|
||||
handler = self.client.secrets.kv.v2
|
||||
if mount not in self.paths.keys():
|
||||
self.paths[mount] = {}
|
||||
try:
|
||||
paths = handler.list_secrets(**args)
|
||||
except hvac.exceptions.InvalidPath:
|
||||
# It's a secret name.
|
||||
_logger.debug('Path {0} on mount {1} is a secret, not a subdir.'.format(path, mount))
|
||||
dpath.util.new(self.paths, fullpath, self.getSecretNames(path, mount, version = version))
|
||||
continue
|
||||
# if 'data' not in paths.keys() or 'keys' not in paths['data'].keys():
|
||||
try:
|
||||
paths_list = paths['data']['keys']
|
||||
except (KeyError, TypeError):
|
||||
_logger.warning('Mount has no secrets/subdirs')
|
||||
_logger.debug('The mount {0} has no secrets or subdirectories'.format(mount))
|
||||
warnings.warn('Mount has no secrets/subdirs')
|
||||
continue
|
||||
for p in paths_list:
|
||||
p_relpath = '/'.join((relpath, p)).replace('//', '/').lstrip('/')
|
||||
p_fullpath = '/'.join((fullpath, p)).replace('//', '/').lstrip('/')
|
||||
_logger.debug(('Recursing getSecretsTree. '
|
||||
'path={0} '
|
||||
'fullpath={1} '
|
||||
'relpath={2} '
|
||||
'p={3} '
|
||||
'p_relpath={4} '
|
||||
'p_fullpath={5}').format(path,
|
||||
fullpath,
|
||||
relpath,
|
||||
p,
|
||||
p_relpath,
|
||||
p_fullpath))
|
||||
self.getSecretsTree(path = p_relpath, mounts = mount)
|
||||
return(None)
|
||||
|
||||
def getSysMounts(self):
|
||||
try:
|
||||
for mount, mount_info in self.client.sys.list_mounted_secrets_engines()['data'].items():
|
||||
@ -62,7 +162,7 @@ class MountHandler(object):
|
||||
_logger.debug('Added mountpoint {0} to mounts list with type {1}'.format(mount, mtype))
|
||||
except hvac.exceptions.Forbidden:
|
||||
_logger.warning('Client does not have permission to read /sys/mounts.')
|
||||
# TODO: should I blindly merge in instead or no?
|
||||
# TODO: should I blindly merge in instead?
|
||||
if self.xml:
|
||||
for mount in self.xml.findall('.//mount'):
|
||||
mname = mount.text
|
||||
@ -72,56 +172,47 @@ class MountHandler(object):
|
||||
_logger.debug('Added mountpoint {0} to mounts list with type {1}'.format(mount, mtype))
|
||||
return(None)
|
||||
|
||||
def getSecrets(self, path = '/', mounts = None):
|
||||
if not mounts:
|
||||
mounts = self.mounts
|
||||
if isinstance(mounts, dict):
|
||||
mounts = list(mounts.keys())
|
||||
if not isinstance(mounts, list):
|
||||
mounts = [mounts]
|
||||
for mount in mounts:
|
||||
mtype = self.mounts.get(mount)
|
||||
if not mtype:
|
||||
_logger.error('Mount not found in defined mounts')
|
||||
_logger.debug('The mount {0} was not found in the defined mounts.'.format(mount))
|
||||
raise ValueError('Mount not found in defined mounts')
|
||||
handler = None
|
||||
if mtype == 'cubbyhole':
|
||||
handler = self.cubbyhandler
|
||||
elif mtype == 'kv':
|
||||
handler = self.client.secrets.kv.v1
|
||||
elif mtype == 'kv2':
|
||||
handler = self.client.secrets.kv.v2
|
||||
if mount not in self.paths.keys():
|
||||
self.paths[mount] = {}
|
||||
try:
|
||||
paths = handler.list_secrets(path = path, mount_point = mount)
|
||||
except hvac.exceptions.InvalidPath:
|
||||
_logger.error('Path does not exist')
|
||||
_logger.debug('Path {0} on mount {1} does not exist.'.format(path, mount))
|
||||
continue
|
||||
if 'data' not in paths.keys() or 'keys' not in paths['data'].keys():
|
||||
_logger.warning('Mount has no secrets/subdirs')
|
||||
_logger.debug('The mount {0} has no secrets or subdirectories'.format(mount))
|
||||
warnings.warn('Mount has no secrets/subdirs')
|
||||
for p2 in paths['data']['keys']:
|
||||
is_dir = False
|
||||
fullpath = '/'.join((path, p2)).replace('//', '/')
|
||||
if p2.endswith('/'):
|
||||
r = _mount_re.search(fullpath)
|
||||
fullpath = r.group('mount')
|
||||
is_dir = True
|
||||
self.paths[mount][fullpath] = None
|
||||
self.getSecrets(path = p2, mounts = mount)
|
||||
sep_p2 = [i for i in fullpath.split('/') if i.strip() != '']
|
||||
if is_dir:
|
||||
def printer(self, output = None, indent = 4):
|
||||
def treePrint(obj, s = 'Password Store\n', level = 0):
|
||||
prefix = '├──'
|
||||
leading_prefix = '│'
|
||||
last_prefix = '└──'
|
||||
pass
|
||||
# print(mount, sep_p2)
|
||||
|
||||
|
||||
def print(self):
|
||||
return(s)
|
||||
if output:
|
||||
output = output.lower()
|
||||
if output and output not in ('pretty', 'yaml', 'json'):
|
||||
# if output and output not in ('pretty', 'yaml', 'json', 'tree'):
|
||||
_logger.error('Invalid output format')
|
||||
_logger.debug('The output parameter ("{0}") must be one of: pretty, yaml, json, or None'.format(output))
|
||||
# _logger.debug(('The output parameter ("{0}") must be one of: '
|
||||
# 'pretty, yaml, json, tree, or None').format(output))
|
||||
raise ValueError('Invalid output format')
|
||||
if output in ('pretty', 'yaml', 'json'):
|
||||
if not any(((indent is None), isinstance(indent, int))):
|
||||
_logger.error('indent parameter must be an integer or None')
|
||||
_logger.debug('The indent parameter ({0}) must be an integer or None'.format(indent))
|
||||
raise ValueError('indent parameter must be an integer or None')
|
||||
if not self.paths:
|
||||
self.getSecretsTree()
|
||||
if output == 'json':
|
||||
import json
|
||||
return(json.dumps(self.paths, indent = indent))
|
||||
elif output == 'yaml':
|
||||
import yaml # https://pypi.org/project/PyYAML/
|
||||
# import pyaml # https://pypi.python.org/pypi/pyaml
|
||||
return(yaml.dump(self.paths, indent = indent))
|
||||
elif output == 'pretty':
|
||||
import pprint
|
||||
pprint.pprint(self.paths)
|
||||
if indent is None:
|
||||
indent = 1
|
||||
return(pprint.pformat(self.paths, indent = indent))
|
||||
# elif output == 'tree':
|
||||
# # UNIX tree command output.
|
||||
# # has prefixes like ├──, │ ├──, └──, etc.
|
||||
# import tree
|
||||
elif not output:
|
||||
return(str(self.paths))
|
||||
return(None)
|
||||
|
||||
def search(self):
|
||||
|
12
vaultpass/tree.py
Normal file
12
vaultpass/tree.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Thanks, dude: https://stackoverflow.com/a/49912639/733214
|
||||
# TODO?
|
||||
|
||||
|
||||
class Tree(object):
|
||||
prefix_middle = '├──'
|
||||
prefix_last = '└──'
|
||||
spacer_middle = (' ' * 4)
|
||||
spacer_last = ('│' + (' ' * 3))
|
||||
parent_fmt = '\033[01;34m{0}/\033[00m'
|
||||
depth = 0
|
||||
|
Reference in New Issue
Block a user