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:
|
one of either:
|
||||||
.... `auth` (see <<Auth>> section below), or
|
.... `auth` (see <<Auth>> section below), or
|
||||||
.... `authGpg`, an <<Auth>> config snippet encrypted with GPG. See the section on <<GPG-Encrypted Elements>>.
|
.... `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.
|
Let's look at an example configuration.
|
||||||
|
|
||||||
@ -78,6 +79,11 @@ Let's look at an example configuration.
|
|||||||
<auth>
|
<auth>
|
||||||
<token/>
|
<token/>
|
||||||
</auth>
|
</auth>
|
||||||
|
<mounts>
|
||||||
|
<mount type="kv1">secret_legacy</mount>
|
||||||
|
<mount type="kv2">secret</mount>
|
||||||
|
<mount type="cubbyhole">cubbyhole</mount>
|
||||||
|
</mounts>
|
||||||
|
|
||||||
</vaultpass>
|
</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 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
|
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
|
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
|
=== Auth
|
||||||
Vault itself supports a https://www.vaultproject.io/docs/auth/[large number of authentication methods^]. However, in
|
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 -->
|
<!-- 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
|
=== GPG-Encrypted Elements
|
||||||
Understandably, in order to have a persistent configuration, that means storing on disk. That also means that they need
|
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
|
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
|
==== GPG-Encrypted Elements Example
|
||||||
|
|
||||||
.`~/.config/vaultpass.xml` snippet:
|
.`~/.config/vaultpass.xml`:
|
||||||
[source,xml]
|
[source,xml]
|
||||||
----
|
----
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
@ -14,8 +14,8 @@
|
|||||||
be able to iterate through available mounts if not. -->
|
be able to iterate through available mounts if not. -->
|
||||||
<!-- Default type if not specified is kv2. -->
|
<!-- Default type if not specified is kv2. -->
|
||||||
<mounts>
|
<mounts>
|
||||||
<mount type="kv">secrets_legacy</mount>
|
<mount type="kv1">secret_legacy</mount>
|
||||||
<mount type="kv2">secrets</mount>
|
<mount type="kv2">secret</mount>
|
||||||
<mount type="cubbyhole">cubbyhole</mount>
|
<mount type="cubbyhole">cubbyhole</mount>
|
||||||
</mounts>
|
</mounts>
|
||||||
</vaultpass>
|
</vaultpass>
|
||||||
|
@ -41,7 +41,7 @@ class PassMan(object):
|
|||||||
return(None)
|
return(None)
|
||||||
|
|
||||||
def _getMount(self):
|
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)
|
self.mount = mounts.MountHandler(self.client, mounts_xml = mounts_xml)
|
||||||
return(None)
|
return(None)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
##
|
##
|
||||||
import dpath # https://pypi.org/project/dpath/
|
import dpath.util # https://pypi.org/project/dpath/
|
||||||
import hvac.exceptions
|
import hvac.exceptions
|
||||||
|
|
||||||
|
|
||||||
@ -23,10 +23,11 @@ class CubbyHandler(object):
|
|||||||
resp = self.client._adapter.list(url = uri)
|
resp = self.client._adapter.list(url = uri)
|
||||||
return(resp.json())
|
return(resp.json())
|
||||||
|
|
||||||
def read_secret(self, *args, **kwargs):
|
def read_secret(self, path, mount_point = 'cubbyhole'):
|
||||||
# https://github.com/hashicorp/vault/issues/8644
|
path = path.lstrip('/')
|
||||||
_logger.warning('Cannot get path info from a cubbyhole')
|
uri = '/v1/{0}/{1}'.format(mount_point, path)
|
||||||
return({'data': {}})
|
resp = self.client._adapter.get(url = uri)
|
||||||
|
return(resp.json())
|
||||||
|
|
||||||
|
|
||||||
class MountHandler(object):
|
class MountHandler(object):
|
||||||
@ -40,6 +41,105 @@ class MountHandler(object):
|
|||||||
self.paths = {}
|
self.paths = {}
|
||||||
self.getSysMounts()
|
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):
|
def getSysMounts(self):
|
||||||
try:
|
try:
|
||||||
for mount, mount_info in self.client.sys.list_mounted_secrets_engines()['data'].items():
|
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))
|
_logger.debug('Added mountpoint {0} to mounts list with type {1}'.format(mount, mtype))
|
||||||
except hvac.exceptions.Forbidden:
|
except hvac.exceptions.Forbidden:
|
||||||
_logger.warning('Client does not have permission to read /sys/mounts.')
|
_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:
|
if self.xml:
|
||||||
for mount in self.xml.findall('.//mount'):
|
for mount in self.xml.findall('.//mount'):
|
||||||
mname = mount.text
|
mname = mount.text
|
||||||
@ -72,56 +172,47 @@ class MountHandler(object):
|
|||||||
_logger.debug('Added mountpoint {0} to mounts list with type {1}'.format(mount, mtype))
|
_logger.debug('Added mountpoint {0} to mounts list with type {1}'.format(mount, mtype))
|
||||||
return(None)
|
return(None)
|
||||||
|
|
||||||
def getSecrets(self, path = '/', mounts = None):
|
def printer(self, output = None, indent = 4):
|
||||||
if not mounts:
|
def treePrint(obj, s = 'Password Store\n', level = 0):
|
||||||
mounts = self.mounts
|
prefix = '├──'
|
||||||
if isinstance(mounts, dict):
|
leading_prefix = '│'
|
||||||
mounts = list(mounts.keys())
|
last_prefix = '└──'
|
||||||
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:
|
|
||||||
pass
|
pass
|
||||||
# print(mount, sep_p2)
|
return(s)
|
||||||
|
if output:
|
||||||
|
output = output.lower()
|
||||||
def print(self):
|
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
|
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)
|
return(None)
|
||||||
|
|
||||||
def search(self):
|
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