checking in

This commit is contained in:
brent s. 2020-04-09 15:47:16 -04:00
parent 861a73ea93
commit 9784b99585
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
6 changed files with 209 additions and 68 deletions

View File

@ -27,7 +27,8 @@ bash and backed by GPG. It's fairly barebones in terms of technology but does a
VaultPass attempts to bridge the gap between the two. It aims to be a drop-in replacement for the pass CLI utility via VaultPass attempts to bridge the gap between the two. It aims to be a drop-in replacement for the pass CLI utility via
subcommands and other operations, but obviously with Vault as a backend instead of GPG-encrypted flatfile hierarchy. subcommands and other operations, but obviously with Vault as a backend instead of GPG-encrypted flatfile hierarchy.


Obviously since the backends are vastly different, total parity is going to be impossible. But I try to get it pretty close. Obviously since the backends are vastly different, total parity is going to be impossible but I try to get it pretty
close. Important deviations are <<known_incompatibilities_with_pass, documented below>>.




== Configuration == Configuration
@ -43,13 +44,13 @@ easier. It's *highly* recommended to use them.]
. The root element (`vaultpass`). . The root element (`vaultpass`).
This element contains attributes describing parsing/validation specifics as well, such as the This element contains attributes describing parsing/validation specifics as well, such as the
https://www.w3.org/TR/xml-names/[namespace definitions^] and https://www.w3.org/TR/xmlschema11-1/#xsi_schemaLocation[schema location^].footnote:confheader[] https://www.w3.org/TR/xml-names/[namespace definitions^] and https://www.w3.org/TR/xmlschema11-1/#xsi_schemaLocation[schema location^].footnote:confheader[]
.. The `server` element.footnote:optelem[This element/attribute/text content is *optional*. See the item's description .. The `server` element. This element is a container for connection and management of the
for how default values/behaviour are determined.] This element is a container for connection and management of the Vault server and is required (even though it may not have any children). This consists of:
Vault server. This consists of: ... A single `uri` element.footnote:optelem[This element/attribute/text content is *optional*. See the item's description
... A single `uri` element.footnote:optelem[] It should be the same as the **base** URL for your Vault server. for how default values/behaviour are determined.] It should be the same as the **base** URL for your Vault server.
The default (if not specified) is to first check for a **`VAULT_SERVER`** environment variable and, if not found, to use If not specified, the default is to first check for a **`VAULT_ADDR`** environment variable and, if not found, to use
`http://localhost:8200/`. `http://localhost:8200/`.
... An unseal directive, which can be used to (attempt to) automatically unseal the server if it is sealed. ... An unseal elementfootnote:optelem[], which can be used to (attempt to) automatically unseal the server if it is sealed.
This isn't required, but can assist in automatic operation. This isn't required, but can assist in automatic operation.
One of either:footnote:optelem[] One of either:footnote:optelem[]
.... `unseal`, the unseal key shard (a Base64 string), or .... `unseal`, the unseal key shard (a Base64 string), or
@ -60,6 +61,9 @@ one of either:
.... `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>>. ... An optional `mounts` container.footnote:optelem[] See the section on <<Mounts>>.


If you would like to initialize Vault with VaultPass, use a self-enclosed <<token>> auth stanza. It will automatically
be replaced once a root token is generated.

Let's look at an example configuration. Let's look at an example configuration.


=== Example Configuration === Example Configuration
@ -88,10 +92,11 @@ Let's look at an example configuration.
</vaultpass> </vaultpass>
---- ----


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). Because we specify mounts, we do not need permissions in Vault to list `/sys/mounts`. needed). Because we specify mounts, we do not need permissions in Vault to list `/sys/mounts` (but if our token has
access to do so per its policy, then any automatically discovered will be added).


=== 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
@ -175,6 +180,10 @@ To determine the behaviour of how this behaves, please refer to the below table.
| 4 |token contained in tags, `source` given |Same as **3**; `source` is ignored. | 4 |token contained in tags, `source` given |Same as **3**; `source` is ignored.
|=== |===


If the Vault instance is not initialized and a `vaultpass init` is called, the configuration file will be updated to
use token auth, populated with the new root token, and populated with the new unseal shard. (The previous configuration
file will be backed up first!).

===== Example Snippet ===== Example Snippet
[source,xml] [source,xml]
---- ----
@ -399,7 +408,7 @@ alias vaultpass='vaultpass -c ~/.config/alternate.vaultpass.xml'


To use the non-aliased command in Bash, you can either invoke the full path: To use the non-aliased command in Bash, you can either invoke the full path:


[source,bash] [source]
---- ----
/usr/local/bin/vaultpass edit path/to/secret /usr/local/bin/vaultpass edit path/to/secret
---- ----
@ -446,19 +455,33 @@ flags/switches to subcommands. **Some** configuration directives/behaviour may b
where supported by Vault/Pass upstream configuration. where supported by Vault/Pass upstream configuration.


=== Vault Paths Don't Match VaultPass' Paths === Vault Paths Don't Match VaultPass' Paths
=== Issue Description ==== Issue Description
Pass and Vault have fundamentally different storage ideas. Pass secrets/passwords are, once decrypted, just plaintext Pass and Vault have fundamentally different storage ideas. Pass secrets/passwords are, once decrypted, just plaintext
blobs. Vault, on the other hand, uses a key/value type of storage. As a result, this means two things: blobs. Vault, on the other hand, uses a key/value type of storage. As a result, this means two things:


* The last item in a path in VaultPass is the key name (e.g. the path `foo/bar/baz` in VaultPass would be a Vault path * The last item in a path in VaultPass is the key name (e.g. the path `foo/bar/baz` in VaultPass would be a Vault path
of `foo/bar`, which would then have a **key** named `baz`), and of `foo/bar`, which would then have a **key** named `baz`), and
* The **`line-number`** sub-argument is completely irrelevant for things like copying to the clipboard and generating a * The **`line-number`** sub-argument is completely irrelevant for things like copying to the clipboard and generating a
QR code (e.g. as in `pass show --clip`**`=line-number`**). QR code (e.g. as in `pass show --clip=line-number`).


==== Workaround(s) ==== Workaround(s)
None, aside from not using the `line-number` sub-argument since it's no longer relevant. (You'll get an error if you None, aside from not using the `line-number` sub-argument since it's no longer relevant. (You'll get an error if you
do.) do.)


=== Unable to specify `line-number`
See <<vault_paths_dont_match_vaultpass_paths, above>> (_Vault Paths Don't Match VaultPass' Paths_).

=== Deleting Secrets in KV2
==== Issue Description
In Pass, because it doesn't have versioning (unless you're using git with your Pass instance). Vault's `kv2` engine,
however, does have versioning. As a result, once a secret is "deleted", it can still be recovered via
https://www.vaultproject.io/docs/secrets/kv/kv-v2/#deleting-and-destroying-data[an `undelete` method^]. If you are
deleting a secret for security reasons, you may want to destroy it instead. VaultPass' delete method uses a delete
rather than a destroy.

==== Workaround(s)
VaultPass has a new subcommand, `destroy`, which will remove versioned secrets **permanently**. Use with caution,
obviously. If called on a non-KV2 mount's path, it will be the same as the `delete` subcommand.


== Submitting a Bug Report/Feature Request == Submitting a Bug Report/Feature Request
Please use https://bugs.square-r00t.net/index.php?do=newtask&project=13[my bugtracker^]. Please use https://bugs.square-r00t.net/index.php?do=newtask&project=13[my bugtracker^].

View File

@ -2,6 +2,7 @@ import logging
import tempfile import tempfile
import os import os
import subprocess import subprocess
import time
## ##
from . import logger from . import logger
_logger = logging.getLogger('VaultPass') _logger = logging.getLogger('VaultPass')
@ -25,13 +26,14 @@ class VaultPass(object):
uri = None uri = None
mount = None mount = None


def __init__(self, mount, cfg = '~/.config/vaultpass.xml'): def __init__(self, initialize = False, cfg = '~/.config/vaultpass.xml'):
self.mname = mount self.initialize = initialize
self.cfg = config.getConfig(cfg) self.cfg = config.getConfig(cfg)
self._getURI() self._getURI()
self.getClient() self.getClient()
self._checkSeal() if not self.initialize:
self._getMount() self._checkSeal()
self._getMount()


def _checkSeal(self): def _checkSeal(self):
_logger.debug('Checking and attempting unseal if necessary and possible.') _logger.debug('Checking and attempting unseal if necessary and possible.')
@ -51,48 +53,47 @@ class VaultPass(object):
raise RuntimeError('Unable to unseal') raise RuntimeError('Unable to unseal')
return(None) return(None)


def _getHandler(self, mount = None, func = 'read', *args, **kwargs): def _getConfirm(self, msg = None):
if func not in ('read', 'write', 'list'): if not msg:
msg = 'Are you sure (y/N)? '
confirm = input(msg)
confirm = confirm.lower().strip()
if confirm.startswith('y'):
return(True)
return(False)

def _getHandler(self, mount, func = 'read', *args, **kwargs):
if func not in ('read', 'write', 'list', 'delete', 'destroy'):
_logger.error('Invalid func') _logger.error('Invalid func')
_logger.debug('Invalid func; must be one of: read, write, list, update') _logger.debug('Invalid func; must be one of: read, write, list, delete, destroy')
raise ValueError('Invalid func') raise ValueError('Invalid func')
if not mount:
mount = self.mname
mtype = self.mount.getMountType(mount) mtype = self.mount.getMountType(mount)
handler = None handler = None
if mtype == 'cubbyhole': handler_map = {'cubbyhole': {'read': self.mount.cubbyhandler.read_secret,
if func == 'read': 'write': self.mount.cubbyhandler.write_secret,
handler = self.mount.cubbyhandler.read_secret 'list': self.mount.cubbyhandler.list_secrets,
elif func == 'write': 'delete': self.mount.cubbyhandler.remove_secret,
handler = self.mount.cubbyhandler.write_secret 'destroy': self.mount.cubbyhandler.remove_secret},
elif func == 'list': 'kv1': {'read': self.client.secrets.kv.v1.read_secret,
handler = self.mount.cubbyhandler.list_secrets 'write': self.client.secrets.kv.v1.create_or_update_secret,
elif mtype == 'kv1': 'list': self.client.secrets.kv.v1.list_secrets,
if func == 'read': 'delete': self.client.secrets.kv.v1.delete_secret,
handler = self.client.secrets.kv.v1.read_secret 'destroy': self.client.secrets.kv.v1.delete_secret},
elif func == 'write': 'kv2': {'read': self.client.secrets.kv.v2.read_secret_version,
handler = self.client.secrets.kv.v1.create_or_update_secret 'write': self.client.secrets.kv.v2.create_or_update_secret,
elif func == 'list': 'list': self.client.secrets.kv.v2.list_secrets,
handler = self.client.secrets.kv.v1.list_secrets 'delete': self.client.secrets.kv.v2.delete_secret_versions,
elif mtype == 'kv2': 'destroy': self.client.secrets.kv.v2.destroy_secret_versions}}
if func == 'read': handler = handler_map.get(mtype, {}).get(func, None)
handler = self.client.secrets.kv.v2.read_secret_version
elif func == 'write':
handler = self.client.secrets.kv.v2.create_or_update_secret
elif func == 'list':
handler = self.client.secrets.kv.v2.list_secrets
if not handler: if not handler:
_logger.error('Could not get handler') _logger.error('Could not get handler')
_logger.debug('Could not get handler for mount {0}'.format(mount)) _logger.debug('Could not get handler for function {0} on mount {1} (type {2})'.format(func, mount, mtype))
raise RuntimeError('Could not get handler') raise RuntimeError('Could not get handler')
return(handler) return(handler)


def _getMount(self): def _getMount(self):
mounts_xml = self.cfg.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)
if self.mname:
# Check that the mount exists
self.mount.getMountType(self.mname)
return(None) return(None)


def _getURI(self): def _getURI(self):
@ -111,13 +112,20 @@ class VaultPass(object):
_logger.debug('Set URI to {0}'.format(self.uri)) _logger.debug('Set URI to {0}'.format(self.uri))
return(None) return(None)


def _pathExists(self, path, mount = None, *args, **kwargs): def _pathExists(self, path, mount, is_secret = False, *args, **kwargs):
if not mount: kname = None
mount = self.mname if is_secret:
exists = False lpath = path.split('/')
if self.mount.getPath(path, mount): path = '/'.join(lpath[0:-1])
exists = True kname = lpath[-1]
return(exists) path_obj = self.mount.getPath(path, mount)
if path_obj:
if not is_secret:
return(True)
else:
if kname in path_obj.keys():
return(True)
return(False)


def convert(self, def convert(self,
mount, mount,
@ -127,16 +135,29 @@ class VaultPass(object):
*args, **kwargs): *args, **kwargs):
pass # TODO pass # TODO


def copySecret(self, oldpath, newpath, mount, newmount, force = False, remove_old = False, *args, **kwargs): def copySecret(self, oldpath, newpath, mount, newmount = None, force = False, remove_old = False, *args, **kwargs):
mtype = self.mount.getMountType(mount) mtype = self.mount.getMountType(mount)
if not newmount:
newmount = mount
newmtype = mtype
else:
newmtype = self.mount.getMountType(newmount)
oldexists = self._pathExists(oldpath, mount = mount) oldexists = self._pathExists(oldpath, mount = mount)
if not oldexists: if not oldexists:
_logger.error('oldpath does not exist') _logger.error('oldpath does not exist')
_logger.debug('The oldpath {0} does not exist'.format(oldpath)) _logger.debug('The oldpath {0} does not exist'.format(oldpath))
raise ValueError('oldpath does not exist') raise ValueError('oldpath does not exist')
data = self.getSecret(oldpath, mount) data = self.getSecret(oldpath, mount)
if not data:
_logger.error('No secret found')
_logger.debug('The secret at path {0} on mount {1} does not exist.'.format(oldpath, mount))
# TODO: left off here # TODO: left off here
newexists = self._pathExists(newpath, mount = mount) newexists = self._pathExists(newpath, mount = newmount)
if newexists and not force:
_logger.debug('The newpath {0} exists; prompting for confirmation.'.format(newpath))
confirm = self._getConfirm('The destination {0} exists. Overwrite (y/N)?'.format(newpath))
if not confirm:
return(None)


if remove_old: if remove_old:
self.deleteSecret(oldpath, mount, force = force) self.deleteSecret(oldpath, mount, force = force)
@ -161,7 +182,16 @@ class VaultPass(object):
resp = handler(**args) resp = handler(**args)
return(resp) return(resp)


def deleteSecret(self, path, mount_name, force = False, recursive = False, *args, **kwargs): def deleteSecret(self, path, mount, force = False, recursive = False, *args, **kwargs):
mtype = self.mount.getMountType(mount)
args = {'path': path,
'mount_point': mount}
handler = self._getHandler(mount, func = 'delete')
is_path = self._pathExists(path, mount)
is_secret = self._pathExists(path, mount, is_secret = True)


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


def editSecret(self, path, mount, editor = constants.EDITOR, *args, **kwargs): def editSecret(self, path, mount, editor = constants.EDITOR, *args, **kwargs):
@ -208,7 +238,7 @@ class VaultPass(object):
_logger.error('Invalid auth configuration') _logger.error('Invalid auth configuration')
raise RuntimeError('Invalid auth configuration') raise RuntimeError('Invalid auth configuration')
self.client = self.auth.client self.client = self.auth.client
if not self.client.sys.is_initialized(): if not self.client.sys.is_initialized() and not self.initialize:
_logger.debug('Vault instance is not initialized. Please initialize (and configure, if necessary) first.') _logger.debug('Vault instance is not initialized. Please initialize (and configure, if necessary) first.')
_logger.error('Not initialized') _logger.error('Not initialized')
raise RuntimeError('Not initialized') raise RuntimeError('Not initialized')
@ -245,6 +275,7 @@ class VaultPass(object):
'qr': qr, 'qr': qr,
'seconds': seconds, 'seconds': seconds,
'printme': printme} 'printme': printme}
# Add return here?
data = self.getSecret(**args) data = self.getSecret(**args)
if qr not in (False, None): if qr not in (False, None):
qrdata, has_x = QR.genQr(data, image = True) qrdata, has_x = QR.genQr(data, image = True)
@ -255,9 +286,14 @@ class VaultPass(object):
fh.write(qrdata.read()) fh.write(qrdata.read())
if printme: if printme:
_logger.debug('Opening {0} in the default image viwer application'.format(fpath)) _logger.debug('Opening {0} in the default image viwer application'.format(fpath))
# We intentionally want this to block, as most image viewers will # We intentionally want this to block, as most image viewers will unload the image once the file
# unload the image once the file is deleted and we can probably # is deleted and we can probably delete it faster than the user can save it elsewhere or
# elete it before the user can save it elsewhere or scan it with their phone. # scan it with their phone.
# TODO: we could use Popen() and do a countdown for "seconds" seconds, and then kill the viewer.
# But that breaks compat with Pass' behaviour.
if printme:
print('Now displaying generated QR code. Please close the viewer when done saving/scanning to '
'securely clean up the generated file...')
cmd = subprocess.run(['xdg-open', fpath], stdout = subprocess.PIPE, stderr = subprocess.PIPE) cmd = subprocess.run(['xdg-open', fpath], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
if cmd.returncode != 0: if cmd.returncode != 0:
_logger.error('xdg-open returned non-zero status code') _logger.error('xdg-open returned non-zero status code')
@ -268,16 +304,35 @@ class VaultPass(object):
o = o.decode('utf-8').strip() o = o.decode('utf-8').strip()
if o != '': if o != '':
_logger.debug('{0}: {1}'.format(x.upper(), o)) _logger.debug('{0}: {1}'.format(x.upper(), o))
if printme:
print('Done. Deleting generated file.')
os.remove(fpath) os.remove(fpath)
elif printme: elif printme:
print(qrdata.read()) print(qrdata.read())
qrdata.seek(0, 0) qrdata.seek(0, 0)
del(qrdata)
if clip not in (False, None): if clip not in (False, None):
clipboard.pasteClipboard(data, seconds = seconds, clipboard = clipboard, printme = printme) clipboard.pasteClipboard(data, seconds = seconds, clipboard = clipboard, printme = printme)
return(data) return(data)


def initVault(self, *args, **kwargs): def initVault(self, *args, **kwargs):
pass # TODO if not self.client.sys.is_initialized():
init_rslt = self.client.sys.initialize(secret_shares = 1, secret_threshold = 1)
unseal = init_rslt['keys_base64'][0]
token = init_rslt['root_token']
self.cfg.updateAuth(unseal, token)
self.client.sys.submit_unseal_key(unseal)
self.client.token = token
# JUST in case.
time.sleep(1)
for mname, mtype in self.mount.mounts.items():
if mtype == 'cubbyhole':
# There isn't a way to "create" a cubbyhole.
continue
self.mount.createMount(mname, mtype)
self._checkSeal()
self._getMount()
return(None)


def insertSecret(self, def insertSecret(self,
path, path,

View File

@ -67,6 +67,9 @@ def parseArgs():
description = ('Delete a secret'), description = ('Delete a secret'),
help = ('Delete a secret'), help = ('Delete a secret'),
aliases = ['remove', 'delete']) aliases = ['remove', 'delete'])
destroy = subparser.add_parser('destroy',
description = ('Destroy a secret permanently'),
help = ('Destroy a secret permanently'))
show = subparser.add_parser('show', show = subparser.add_parser('show',
description = ('Print/fetch a secret'), description = ('Print/fetch a secret'),
help = ('Print/fetch a secret')) help = ('Print/fetch a secret'))
@ -399,7 +402,7 @@ def parseArgs():
rm.add_argument('-r', '--recursive', rm.add_argument('-r', '--recursive',
dest = 'recurse', dest = 'recurse',
action = 'store_true', action = 'store_true',
help = ('If PATH/TO/SECRET is a directory, delete all subentries')) help = ('If PATH/TO/SECRET is a directory, delete all subentries along with the path itself'))
rm.add_argument('-f', '--force', rm.add_argument('-f', '--force',
dest = 'force', dest = 'force',
action = 'store_true', action = 'store_true',
@ -407,6 +410,18 @@ def parseArgs():
rm.add_argument('path', rm.add_argument('path',
metavar = 'PATH/TO/SECRET', metavar = 'PATH/TO/SECRET',
help = ('The path to the secret or subdirectory')) help = ('The path to the secret or subdirectory'))
# DESTROY
destroy.add_argument('-r', '--recursive',
dest = 'recurse',
action = 'store_true',
help = ('If PATH/TO/SECRET is a directory, delete all subentries along with the path itself'))
destroy.add_argument('-f', '--force',
dest = 'force',
action = 'store_true',
help = ('If specified, delete all matching path(s) without prompting for confirmation'))
destroy.add_argument('path',
metavar = 'PATH/TO/SECRET',
help = ('The path to the secret or subdirectory'))
# SHOW # SHOW
# vp.getSecret(printme = True) # vp.getSecret(printme = True)
# TODO: does the default overwrite the None if not specified? # TODO: does the default overwrite the None if not specified?

View File

@ -1,5 +1,6 @@
import logging import logging
import os import os
import warnings
## ##
import hvac import hvac


@ -111,7 +112,7 @@ class Token(_AuthBase):
_logger.debug(('Environment variable {0} was specified as containing the token ' _logger.debug(('Environment variable {0} was specified as containing the token '
'but it is empty').format(env_var)) 'but it is empty').format(env_var))
_logger.error('Env var not populated') _logger.error('Env var not populated')
raise RuntimeError('Env var not populated') raise OSError('Env var not populated')
return(var) return(var)


def _getFile(self, fpath): def _getFile(self, fpath):
@ -122,6 +123,7 @@ class Token(_AuthBase):


def getClient(self): def getClient(self):
_token = self.xml.text _token = self.xml.text
chk = True
if _token is not None: if _token is not None:
self.token = _token self.token = _token
else: else:
@ -134,17 +136,20 @@ class Token(_AuthBase):
try: try:
self._getEnv('VAULT_TOKEN') self._getEnv('VAULT_TOKEN')
break break
except Exception as e: except OSError as e:
pass pass
try: try:
self._getFile('~/.vault-token') self._getFile('~/.vault-token')
_exhausted = True
except Exception as e: except Exception as e:
_exhausted = True _exhausted = True
if not self.token: if not self.token:
_logger.debug(('Unable to automatically determine token from ' _logger.debug(('Unable to automatically determine token from '
'environment variable or filesystem defaults')) 'environment variable or filesystem defaults. Ignore this if you are initializing '
_logger.error('Cannot determine token') 'Vault.'))
raise RuntimeError('Cannot determine token') _logger.warning('Cannot determine token')
warnings.warn('Cannot determine token')
chk = False
else: else:
if a.startswith('env:'): if a.startswith('env:'):
e = a.split(':', 1) e = a.split(':', 1)
@ -156,7 +161,8 @@ class Token(_AuthBase):
_logger.info('Initialized client.') _logger.info('Initialized client.')
self.client.token = self.token self.client.token = self.token
_logger.debug('Applied token.') _logger.debug('Applied token.')
self.authCheck() if chk:
self.authCheck()
return(None) return(None)





View File

@ -1,7 +1,9 @@
import copy import copy
import datetime
import os import os
import logging import logging
import re import re
import shutil
## ##
from . import gpg_handler from . import gpg_handler
import requests import requests
@ -217,6 +219,40 @@ class Config(object):
_logger.debug('Rendered string output successfully.') _logger.debug('Rendered string output successfully.')
return(strxml) return(strxml)


def updateAuth(self, unseal_shard, token):
nsmap = self.namespaced_xml.nsmap
unseal_ns_xml = self.namespaced_xml.find('.//{{{0}}}unseal'.format(nsmap[None]))
unseal_xml = self.xml.find('.//unseal')
auth_ns_xml = self.namespaced_xml.find('.//{{{0}}}auth'.format(nsmap[None]))
auth_xml = self.xml.find('.//auth')
token_ns_xml = auth_ns_xml.find('.//{{{0}}}token'.format(nsmap[None]))
token_xml = auth_xml.find('.//token')
if token_xml is None:
# Config is using a non-token auth, so we replace it.
newauth_xml = etree.Element('auth')
newauth_ns_xml = etree.Element('auth', nsmap = nsmap)
token_xml = etree.SubElement(newauth_xml, 'token')
token_ns_xml = etree.SubElement(newauth_ns_xml, 'token', nsmap = nsmap)
auth_xml.getparent().replace(auth_xml, newauth_xml)
auth_ns_xml.getparent().replace(auth_ns_xml, newauth_ns_xml)
if unseal_xml is None:
# And we need to add the unseal as well.
server_xml = self.xml.find('.//server')
server_ns_xml = self.namespaced_xml.find('.//{{{0}}}server'.format(nsmap[None]))
unseal_xml = etree.SubElement(server_xml, 'unseal')
unseal_ns_xml = etree.SubElement(server_ns_xml, 'unseal')
unseal_xml.text = unseal_shard
unseal_ns_xml.text = unseal_shard
token_xml.text = token
token_ns_xml.text = token
self.parse()
if isinstance(self, LocalFile):
bakpath = '{0}.bak_{1}'.format(self.source, datetime.datetime.utcnow().timestamp())
shutil.copy(self.source, bakpath)
with open(self.source, 'wb') as fh:
fh.write(self.toString())
return(None)

def validate(self): def validate(self):
if not self.xsd: if not self.xsd:
self.getXSD() self.getXSD()

View File

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


def remove_secret(self, path, mount_point = 'cubbyhole', *args, **kwargs):
path = path.lstrip('/')
uri = '{0}/{1}'.format(mount_point, path)
resp = self.client._adapter.delete(url = uri)
return(resp.json())

def write_secret(self, path, secret, mount_point = 'cubbyhole', *args, **kwargs): def write_secret(self, path, secret, mount_point = 'cubbyhole', *args, **kwargs):
path = path.lstrip('/') path = path.lstrip('/')
args = {'path': '/'.join((mount_point, path))} args = {'path': '/'.join((mount_point, path))}