Vault based key manager
* Uses https://www.vaultproject.io/ to store/fetch secrets * All we need is the URL and a Token to talk to the vault server * tox target "functional-vault" sets up a server in development mode and runs functional tests * Supports both http:// and https:// url(s) * the https support was tested by setting up a vault server by hand (https://gist.github.com/dims/47674cf2c3b0a953df69246c2ea1ff78) * create_key_pair is the only API that is not implemented Change-Id: I6436e5841c8e77a7262b4d5aa39201b40a985255
This commit is contained in:
parent
5bc58116c9
commit
a972da32a9
@ -25,8 +25,9 @@ key_manager_opts = [
|
|||||||
default='barbican',
|
default='barbican',
|
||||||
deprecated_name='api_class',
|
deprecated_name='api_class',
|
||||||
deprecated_group='key_manager',
|
deprecated_group='key_manager',
|
||||||
help='Specify the key manager implementation. Default is '
|
help='Specify the key manager implementation. Options are '
|
||||||
'"barbican".Will support the values earlier set using '
|
'"barbican" and "vault". Default is "barbican". Will '
|
||||||
|
'support the values earlier set using '
|
||||||
'[key_manager]/api_class for some time.'),
|
'[key_manager]/api_class for some time.'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
297
castellan/key_manager/vault_key_manager.py
Normal file
297
castellan/key_manager/vault_key_manager.py
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Key manager implementation for Vault
|
||||||
|
"""
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from keystoneauth1 import loading
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import requests
|
||||||
|
import six
|
||||||
|
|
||||||
|
from castellan.common import exception
|
||||||
|
from castellan.common.objects import opaque_data as op_data
|
||||||
|
from castellan.common.objects import passphrase
|
||||||
|
from castellan.common.objects import private_key as pri_key
|
||||||
|
from castellan.common.objects import public_key as pub_key
|
||||||
|
from castellan.common.objects import symmetric_key as sym_key
|
||||||
|
from castellan.common.objects import x_509
|
||||||
|
from castellan.i18n import _
|
||||||
|
from castellan.key_manager import key_manager
|
||||||
|
|
||||||
|
DEFAULT_VAULT_URL = "http://127.0.0.1:8200"
|
||||||
|
|
||||||
|
vault_opts = [
|
||||||
|
cfg.StrOpt('root_token_id',
|
||||||
|
help='root token for vault'),
|
||||||
|
cfg.StrOpt('vault_url',
|
||||||
|
default=DEFAULT_VAULT_URL,
|
||||||
|
help='Use this endpoint to connect to Vault, for example: '
|
||||||
|
'"%s"' % DEFAULT_VAULT_URL),
|
||||||
|
cfg.StrOpt('ssl_ca_crt_file',
|
||||||
|
help='Absolute path to ca cert file'),
|
||||||
|
cfg.BoolOpt('use_ssl',
|
||||||
|
default=False,
|
||||||
|
help=_('SSL Enabled/Disabled')),
|
||||||
|
]
|
||||||
|
|
||||||
|
VAULT_OPT_GROUP = 'vault'
|
||||||
|
|
||||||
|
_EXCEPTIONS_BY_CODE = [
|
||||||
|
requests.codes['internal_server_error'],
|
||||||
|
requests.codes['service_unavailable'],
|
||||||
|
requests.codes['request_timeout'],
|
||||||
|
requests.codes['gateway_timeout'],
|
||||||
|
requests.codes['precondition_failed'],
|
||||||
|
]
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VaultKeyManager(key_manager.KeyManager):
|
||||||
|
"""Key Manager Interface that wraps the Vault REST API."""
|
||||||
|
|
||||||
|
_secret_type_dict = {
|
||||||
|
op_data.OpaqueData: 'opaque',
|
||||||
|
passphrase.Passphrase: 'passphrase',
|
||||||
|
pri_key.PrivateKey: 'private',
|
||||||
|
pub_key.PublicKey: 'public',
|
||||||
|
sym_key.SymmetricKey: 'symmetric',
|
||||||
|
x_509.X509: 'certificate'}
|
||||||
|
|
||||||
|
def __init__(self, configuration):
|
||||||
|
self._conf = configuration
|
||||||
|
self._conf.register_opts(vault_opts, group=VAULT_OPT_GROUP)
|
||||||
|
loading.register_session_conf_options(self._conf, VAULT_OPT_GROUP)
|
||||||
|
self._session = requests.Session()
|
||||||
|
self._root_token_id = self._conf.vault.root_token_id
|
||||||
|
self._vault_url = self._conf.vault.vault_url
|
||||||
|
if self._vault_url.startswith("https://"):
|
||||||
|
self._verify_server = self._conf.vault.ssl_ca_crt_file or True
|
||||||
|
else:
|
||||||
|
self._verify_server = False
|
||||||
|
|
||||||
|
def _get_url(self):
|
||||||
|
if not self._vault_url.endswith('/'):
|
||||||
|
self._vault_url += '/'
|
||||||
|
return self._vault_url
|
||||||
|
|
||||||
|
def create_key_pair(self, context, algorithm, length,
|
||||||
|
expiration=None, name=None):
|
||||||
|
"""Creates an asymmetric key pair."""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"VaultKeyManager does not support asymmetric keys")
|
||||||
|
|
||||||
|
def _store_key_value(self, key_id, value):
|
||||||
|
|
||||||
|
type_value = self._secret_type_dict.get(type(value))
|
||||||
|
if type_value is None:
|
||||||
|
raise exception.KeyManagerError(
|
||||||
|
"Unknown type for value : %r" % value)
|
||||||
|
|
||||||
|
headers = {'X-Vault-Token': self._root_token_id}
|
||||||
|
try:
|
||||||
|
resource_url = self._get_url() + 'v1/secret/' + key_id
|
||||||
|
record = {
|
||||||
|
'type': type_value,
|
||||||
|
'value': binascii.hexlify(value.get_encoded()).decode('utf-8'),
|
||||||
|
'algorithm': (value.algorithm if hasattr(value, 'algorithm')
|
||||||
|
else None),
|
||||||
|
'bit_length': (value.bit_length if hasattr(value, 'bit_length')
|
||||||
|
else None),
|
||||||
|
'name': value.name,
|
||||||
|
'created': value.created
|
||||||
|
}
|
||||||
|
resp = self._session.post(resource_url,
|
||||||
|
verify=self._verify_server,
|
||||||
|
json=record,
|
||||||
|
headers=headers)
|
||||||
|
except requests.exceptions.Timeout as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
except Exception as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
|
||||||
|
if resp.status_code in _EXCEPTIONS_BY_CODE:
|
||||||
|
raise exception.KeyManagerError(resp.reason)
|
||||||
|
if resp.status_code == requests.codes['forbidden']:
|
||||||
|
raise exception.Forbidden()
|
||||||
|
|
||||||
|
return key_id
|
||||||
|
|
||||||
|
def create_key(self, context, algorithm, length, name=None, **kwargs):
|
||||||
|
"""Creates a symmetric key."""
|
||||||
|
|
||||||
|
# Confirm context is provided, if not raise forbidden
|
||||||
|
if not context:
|
||||||
|
msg = _("User is not authorized to use key manager.")
|
||||||
|
raise exception.Forbidden(msg)
|
||||||
|
|
||||||
|
key_id = uuid.uuid4().hex
|
||||||
|
key_value = os.urandom(length or 32)
|
||||||
|
key = sym_key.SymmetricKey(algorithm,
|
||||||
|
length or 32,
|
||||||
|
key_value,
|
||||||
|
key_id,
|
||||||
|
name or int(time.time()))
|
||||||
|
return self._store_key_value(key_id, key)
|
||||||
|
|
||||||
|
def store(self, context, key_value, **kwargs):
|
||||||
|
"""Stores (i.e., registers) a key with the key manager."""
|
||||||
|
|
||||||
|
# Confirm context is provided, if not raise forbidden
|
||||||
|
if not context:
|
||||||
|
msg = _("User is not authorized to use key manager.")
|
||||||
|
raise exception.Forbidden(msg)
|
||||||
|
|
||||||
|
key_id = uuid.uuid4().hex
|
||||||
|
return self._store_key_value(key_id, key_value)
|
||||||
|
|
||||||
|
def get(self, context, key_id, metadata_only=False):
|
||||||
|
"""Retrieves the key identified by the specified id."""
|
||||||
|
|
||||||
|
# Confirm context is provided, if not raise forbidden
|
||||||
|
if not context:
|
||||||
|
msg = _("User is not authorized to use key manager.")
|
||||||
|
raise exception.Forbidden(msg)
|
||||||
|
|
||||||
|
if not key_id:
|
||||||
|
raise exception.KeyManagerError('key identifier not provided')
|
||||||
|
|
||||||
|
headers = {'X-Vault-Token': self._root_token_id}
|
||||||
|
try:
|
||||||
|
resource_url = self._get_url() + 'v1/secret/' + key_id
|
||||||
|
resp = self._session.get(resource_url,
|
||||||
|
verify=self._verify_server,
|
||||||
|
headers=headers)
|
||||||
|
except requests.exceptions.Timeout as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
except Exception as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
|
||||||
|
if resp.status_code in _EXCEPTIONS_BY_CODE:
|
||||||
|
raise exception.KeyManagerError(resp.reason)
|
||||||
|
if resp.status_code == requests.codes['forbidden']:
|
||||||
|
raise exception.Forbidden()
|
||||||
|
if resp.status_code == requests.codes['not_found']:
|
||||||
|
raise exception.ManagedObjectNotFoundError(uuid=key_id)
|
||||||
|
|
||||||
|
record = resp.json()['data']
|
||||||
|
key = None if metadata_only else binascii.unhexlify(record['value'])
|
||||||
|
|
||||||
|
clazz = None
|
||||||
|
for type_clazz, type_name in self._secret_type_dict.items():
|
||||||
|
if type_name == record['type']:
|
||||||
|
clazz = type_clazz
|
||||||
|
|
||||||
|
if clazz is None:
|
||||||
|
raise exception.KeyManagerError(
|
||||||
|
"Unknown type : %r" % record['type'])
|
||||||
|
|
||||||
|
if hasattr(clazz, 'algorithm') and hasattr(clazz, 'bit_length'):
|
||||||
|
return clazz(record['algorithm'],
|
||||||
|
record['bit_length'],
|
||||||
|
key,
|
||||||
|
record['name'],
|
||||||
|
record['created'],
|
||||||
|
key_id)
|
||||||
|
else:
|
||||||
|
return clazz(key,
|
||||||
|
record['name'],
|
||||||
|
record['created'],
|
||||||
|
key_id)
|
||||||
|
|
||||||
|
def delete(self, context, key_id):
|
||||||
|
"""Represents deleting the key."""
|
||||||
|
|
||||||
|
# Confirm context is provided, if not raise forbidden
|
||||||
|
if not context:
|
||||||
|
msg = _("User is not authorized to use key manager.")
|
||||||
|
raise exception.Forbidden(msg)
|
||||||
|
|
||||||
|
if not key_id:
|
||||||
|
raise exception.KeyManagerError('key identifier not provided')
|
||||||
|
|
||||||
|
headers = {'X-Vault-Token': self._root_token_id}
|
||||||
|
try:
|
||||||
|
resource_url = self._get_url() + 'v1/secret/' + key_id
|
||||||
|
resp = self._session.delete(resource_url,
|
||||||
|
verify=self._verify_server,
|
||||||
|
headers=headers)
|
||||||
|
except requests.exceptions.Timeout as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
except Exception as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
|
||||||
|
if resp.status_code in _EXCEPTIONS_BY_CODE:
|
||||||
|
raise exception.KeyManagerError(resp.reason)
|
||||||
|
if resp.status_code == requests.codes['forbidden']:
|
||||||
|
raise exception.Forbidden()
|
||||||
|
if resp.status_code == requests.codes['not_found']:
|
||||||
|
raise exception.ManagedObjectNotFoundError(uuid=key_id)
|
||||||
|
|
||||||
|
def list(self, context, object_type=None, metadata_only=False):
|
||||||
|
"""Lists the managed objects given the criteria."""
|
||||||
|
|
||||||
|
# Confirm context is provided, if not raise forbidden
|
||||||
|
if not context:
|
||||||
|
msg = _("User is not authorized to use key manager.")
|
||||||
|
raise exception.Forbidden(msg)
|
||||||
|
|
||||||
|
if object_type and object_type not in self._secret_type_dict:
|
||||||
|
msg = _("Invalid secret type: %s") % object_type
|
||||||
|
raise exception.KeyManagerError(reason=msg)
|
||||||
|
|
||||||
|
headers = {'X-Vault-Token': self._root_token_id}
|
||||||
|
try:
|
||||||
|
resource_url = self._get_url() + 'v1/secret/?list=true'
|
||||||
|
resp = self._session.get(resource_url,
|
||||||
|
verify=self._verify_server,
|
||||||
|
headers=headers)
|
||||||
|
keys = resp.json()['data']['keys']
|
||||||
|
except requests.exceptions.Timeout as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
except Exception as ex:
|
||||||
|
raise exception.KeyManagerError(six.text_type(ex))
|
||||||
|
|
||||||
|
if resp.status_code in _EXCEPTIONS_BY_CODE:
|
||||||
|
raise exception.KeyManagerError(resp.reason)
|
||||||
|
if resp.status_code == requests.codes['forbidden']:
|
||||||
|
raise exception.Forbidden()
|
||||||
|
if resp.status_code == requests.codes['not_found']:
|
||||||
|
keys = []
|
||||||
|
|
||||||
|
objects = []
|
||||||
|
for obj_id in keys:
|
||||||
|
try:
|
||||||
|
obj = self.get(context, obj_id, metadata_only=metadata_only)
|
||||||
|
if object_type is None or isinstance(obj, object_type):
|
||||||
|
objects.append(obj)
|
||||||
|
except exception.ManagedObjectNotFoundError as e:
|
||||||
|
LOG.warning(_("Error occurred while retrieving object "
|
||||||
|
"metadata, not adding it to the list: %s"), e)
|
||||||
|
pass
|
||||||
|
return objects
|
@ -20,6 +20,12 @@ try:
|
|||||||
from castellan.key_manager import barbican_key_manager as bkm
|
from castellan.key_manager import barbican_key_manager as bkm
|
||||||
except ImportError:
|
except ImportError:
|
||||||
bkm = None
|
bkm = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from castellan.key_manager import vault_key_manager as vkm
|
||||||
|
except ImportError:
|
||||||
|
vkm = None
|
||||||
|
|
||||||
from castellan.common import utils
|
from castellan.common import utils
|
||||||
|
|
||||||
_DEFAULT_LOG_LEVELS = ['castellan=WARN']
|
_DEFAULT_LOG_LEVELS = ['castellan=WARN']
|
||||||
@ -33,7 +39,8 @@ _DEFAULT_LOGGING_CONTEXT_FORMAT = ('%(asctime)s.%(msecs)03d %(process)d '
|
|||||||
def set_defaults(conf, backend=None, barbican_endpoint=None,
|
def set_defaults(conf, backend=None, barbican_endpoint=None,
|
||||||
barbican_api_version=None, auth_endpoint=None,
|
barbican_api_version=None, auth_endpoint=None,
|
||||||
retry_delay=None, number_of_retries=None, verify_ssl=None,
|
retry_delay=None, number_of_retries=None, verify_ssl=None,
|
||||||
api_class=None):
|
api_class=None, vault_root_token_id=None, vault_url=None,
|
||||||
|
vault_ssl_ca_crt_file=None, vault_use_ssl=None):
|
||||||
"""Set defaults for configuration values.
|
"""Set defaults for configuration values.
|
||||||
|
|
||||||
Overrides the default options values.
|
Overrides the default options values.
|
||||||
@ -45,10 +52,16 @@ def set_defaults(conf, backend=None, barbican_endpoint=None,
|
|||||||
:param retry_delay: Use this attribute to set retry delay.
|
:param retry_delay: Use this attribute to set retry delay.
|
||||||
:param number_of_retries: Use this attribute to set number of retries.
|
:param number_of_retries: Use this attribute to set number of retries.
|
||||||
:param verify_ssl: Use this to specify if ssl should be verified.
|
:param verify_ssl: Use this to specify if ssl should be verified.
|
||||||
|
:param vault_root_token_id: Use this for the root token id for vault.
|
||||||
|
:param vault_url: Use this for the url for vault.
|
||||||
|
:param vault_use_ssl: Use this to force vault driver to use ssl.
|
||||||
|
:param vault_ssl_ca_crt_file: Use this for the CA file for vault.
|
||||||
"""
|
"""
|
||||||
conf.register_opts(km.key_manager_opts, group='key_manager')
|
conf.register_opts(km.key_manager_opts, group='key_manager')
|
||||||
if bkm:
|
if bkm:
|
||||||
conf.register_opts(bkm.barbican_opts, group=bkm.BARBICAN_OPT_GROUP)
|
conf.register_opts(bkm.barbican_opts, group=bkm.BARBICAN_OPT_GROUP)
|
||||||
|
if vkm:
|
||||||
|
conf.register_opts(vkm.vault_opts, group=vkm.VAULT_OPT_GROUP)
|
||||||
|
|
||||||
# Use the new backend option if set or fall back to the older api_class
|
# Use the new backend option if set or fall back to the older api_class
|
||||||
default_backend = backend or api_class
|
default_backend = backend or api_class
|
||||||
@ -75,6 +88,20 @@ def set_defaults(conf, backend=None, barbican_endpoint=None,
|
|||||||
conf.set_default('verify_ssl', verify_ssl,
|
conf.set_default('verify_ssl', verify_ssl,
|
||||||
group=bkm.BARBICAN_OPT_GROUP)
|
group=bkm.BARBICAN_OPT_GROUP)
|
||||||
|
|
||||||
|
if vkm is not None:
|
||||||
|
if vault_root_token_id is not None:
|
||||||
|
conf.set_default('root_token_id', vault_root_token_id,
|
||||||
|
group=vkm.VAULT_OPT_GROUP)
|
||||||
|
if vault_url is not None:
|
||||||
|
conf.set_default('vault_url', vault_url,
|
||||||
|
group=vkm.VAULT_OPT_GROUP)
|
||||||
|
if vault_ssl_ca_crt_file is not None:
|
||||||
|
conf.set_default('ssl_ca_crt_file', vault_ssl_ca_crt_file,
|
||||||
|
group=vkm.VAULT_OPT_GROUP)
|
||||||
|
if vault_use_ssl is not None:
|
||||||
|
conf.set_default('use_ssl', vault_use_ssl,
|
||||||
|
group=vkm.VAULT_OPT_GROUP)
|
||||||
|
|
||||||
|
|
||||||
def enable_logging(conf=None, app_name='castellan'):
|
def enable_logging(conf=None, app_name='castellan'):
|
||||||
conf = conf or cfg.CONF
|
conf = conf or cfg.CONF
|
||||||
@ -109,4 +136,6 @@ def list_opts():
|
|||||||
|
|
||||||
if bkm is not None:
|
if bkm is not None:
|
||||||
opts.append((bkm.BARBICAN_OPT_GROUP, bkm.barbican_opts))
|
opts.append((bkm.BARBICAN_OPT_GROUP, bkm.barbican_opts))
|
||||||
|
if vkm is not None:
|
||||||
|
opts.append((vkm.VAULT_OPT_GROUP, vkm.vault_opts))
|
||||||
return opts
|
return opts
|
||||||
|
@ -26,6 +26,7 @@ from oslo_config import cfg
|
|||||||
from oslo_context import context
|
from oslo_context import context
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
from oslotest import base
|
from oslotest import base
|
||||||
|
from testtools import testcase
|
||||||
|
|
||||||
from castellan.common.credentials import keystone_password
|
from castellan.common.credentials import keystone_password
|
||||||
from castellan.common.credentials import keystone_token
|
from castellan.common.credentials import keystone_token
|
||||||
@ -50,7 +51,13 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(BarbicanKeyManagerTestCase, self).setUp()
|
super(BarbicanKeyManagerTestCase, self).setUp()
|
||||||
|
try:
|
||||||
self.ctxt = self.get_context()
|
self.ctxt = self.get_context()
|
||||||
|
self.key_mgr._get_barbican_client(self.ctxt)
|
||||||
|
except Exception as e:
|
||||||
|
# When we run functional-vault target, This test class needs
|
||||||
|
# to be skipped as barbican is not running
|
||||||
|
raise testcase.TestSkipped(str(e))
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(BarbicanKeyManagerTestCase, self).tearDown()
|
super(BarbicanKeyManagerTestCase, self).tearDown()
|
||||||
|
108
castellan/tests/functional/key_manager/test_vault_key_manager.py
Normal file
108
castellan/tests/functional/key_manager/test_vault_key_manager.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Functional test cases for the Vault key manager.
|
||||||
|
|
||||||
|
Note: This requires local running instance of Vault.
|
||||||
|
"""
|
||||||
|
import abc
|
||||||
|
import os
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_context import context
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
from oslotest import base
|
||||||
|
from testtools import testcase
|
||||||
|
|
||||||
|
from castellan.common import exception
|
||||||
|
from castellan.key_manager import vault_key_manager
|
||||||
|
from castellan.tests.functional import config
|
||||||
|
from castellan.tests.functional.key_manager import test_key_manager
|
||||||
|
|
||||||
|
CONF = config.get_config()
|
||||||
|
|
||||||
|
|
||||||
|
class VaultKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
|
||||||
|
def _create_key_manager(self):
|
||||||
|
key_mgr = vault_key_manager.VaultKeyManager(cfg.CONF)
|
||||||
|
|
||||||
|
if ('VAULT_TEST_URL' not in os.environ or
|
||||||
|
'VAULT_TEST_ROOT_TOKEN' not in os.environ):
|
||||||
|
raise testcase.TestSkipped('Missing Vault setup information')
|
||||||
|
|
||||||
|
key_mgr._root_token_id = os.environ['VAULT_TEST_ROOT_TOKEN']
|
||||||
|
key_mgr._vault_url = os.environ['VAULT_TEST_URL']
|
||||||
|
return key_mgr
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_context(self):
|
||||||
|
"""Retrieves Context for Authentication"""
|
||||||
|
return
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(VaultKeyManagerTestCase, self).setUp()
|
||||||
|
self.ctxt = self.get_context()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super(VaultKeyManagerTestCase, self).tearDown()
|
||||||
|
|
||||||
|
def test_create_key_pair(self):
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.key_mgr.create_key_pair, None, None, None)
|
||||||
|
|
||||||
|
def test_create_null_context(self):
|
||||||
|
self.assertRaises(exception.Forbidden,
|
||||||
|
self.key_mgr.create_key, None, 'AES', 256)
|
||||||
|
|
||||||
|
def test_create_key_pair_null_context(self):
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.key_mgr.create_key_pair, None, 'RSA', 2048)
|
||||||
|
|
||||||
|
def test_delete_null_context(self):
|
||||||
|
key_uuid = self._get_valid_object_uuid(
|
||||||
|
test_key_manager._get_test_symmetric_key())
|
||||||
|
self.addCleanup(self.key_mgr.delete, self.ctxt, key_uuid)
|
||||||
|
self.assertRaises(exception.Forbidden,
|
||||||
|
self.key_mgr.delete, None, key_uuid)
|
||||||
|
|
||||||
|
def test_delete_null_object(self):
|
||||||
|
self.assertRaises(exception.KeyManagerError,
|
||||||
|
self.key_mgr.delete, self.ctxt, None)
|
||||||
|
|
||||||
|
def test_get_null_context(self):
|
||||||
|
key_uuid = self._get_valid_object_uuid(
|
||||||
|
test_key_manager._get_test_symmetric_key())
|
||||||
|
self.addCleanup(self.key_mgr.delete, self.ctxt, key_uuid)
|
||||||
|
self.assertRaises(exception.Forbidden,
|
||||||
|
self.key_mgr.get, None, key_uuid)
|
||||||
|
|
||||||
|
def test_get_null_object(self):
|
||||||
|
self.assertRaises(exception.KeyManagerError,
|
||||||
|
self.key_mgr.get, self.ctxt, None)
|
||||||
|
|
||||||
|
def test_get_unknown_key(self):
|
||||||
|
bad_key_uuid = uuidutils.generate_uuid()
|
||||||
|
self.assertRaises(exception.ManagedObjectNotFoundError,
|
||||||
|
self.key_mgr.get, self.ctxt, bad_key_uuid)
|
||||||
|
|
||||||
|
def test_store_null_context(self):
|
||||||
|
key = test_key_manager._get_test_symmetric_key()
|
||||||
|
|
||||||
|
self.assertRaises(exception.Forbidden,
|
||||||
|
self.key_mgr.store, None, key)
|
||||||
|
|
||||||
|
|
||||||
|
class VaultKeyManagerOSLOContextTestCase(VaultKeyManagerTestCase,
|
||||||
|
base.BaseTestCase):
|
||||||
|
def get_context(self):
|
||||||
|
return context.get_admin_context()
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added a new provider for Vault (https://www.vaultproject.io/)
|
@ -29,6 +29,7 @@ oslo.config.opts =
|
|||||||
|
|
||||||
castellan.drivers =
|
castellan.drivers =
|
||||||
barbican = castellan.key_manager.barbican_key_manager:BarbicanKeyManager
|
barbican = castellan.key_manager.barbican_key_manager:BarbicanKeyManager
|
||||||
|
vault = castellan.key_manager.vault_key_manager:VaultKeyManager
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = doc/source
|
||||||
|
31
tools/setup-vault-env.sh
Executable file
31
tools/setup-vault-env.sh
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -eux
|
||||||
|
if [ -z "$(which vault)" ]; then
|
||||||
|
VAULT_VERSION=0.7.3
|
||||||
|
SUFFIX=zip
|
||||||
|
case `uname -s` in
|
||||||
|
Darwin)
|
||||||
|
OS=darwin
|
||||||
|
;;
|
||||||
|
Linux)
|
||||||
|
OS=linux
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported OS"
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
case `uname -m` in
|
||||||
|
x86_64)
|
||||||
|
MACHINE=amd64
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported machine"
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
TARBALL_NAME=vault_${VAULT_VERSION}_${OS}_${MACHINE}
|
||||||
|
test ! -d "$TARBALL_NAME" && mkdir ${TARBALL_NAME} && wget https://releases.hashicorp.com/vault/${VAULT_VERSION}/${TARBALL_NAME}.${SUFFIX} && unzip -d ${TARBALL_NAME} ${TARBALL_NAME}.${SUFFIX} && rm ${TARBALL_NAME}.${SUFFIX}
|
||||||
|
export VAULT_CONFIG_PATH=$(pwd)/$TARBALL_NAME/vault.json
|
||||||
|
export PATH=$PATH:$(pwd)/$TARBALL_NAME
|
||||||
|
fi
|
||||||
|
|
||||||
|
$*
|
12
tox.ini
12
tox.ini
@ -57,6 +57,18 @@ deps = -r{toxinidir}/requirements.txt
|
|||||||
-r{toxinidir}/test-requirements.txt
|
-r{toxinidir}/test-requirements.txt
|
||||||
commands = python setup.py testr --slowest --testr-args='{posargs}'
|
commands = python setup.py testr --slowest --testr-args='{posargs}'
|
||||||
|
|
||||||
|
[testenv:functional-vault]
|
||||||
|
passenv = HOME
|
||||||
|
usedevelop = True
|
||||||
|
install_command = pip install -U {opts} {packages}
|
||||||
|
setenv =
|
||||||
|
VIRTUAL_ENV={envdir}
|
||||||
|
OS_TEST_PATH=./castellan/tests/functional
|
||||||
|
deps = -r{toxinidir}/requirements.txt
|
||||||
|
-r{toxinidir}/test-requirements.txt
|
||||||
|
commands =
|
||||||
|
{toxinidir}/tools/setup-vault-env.sh pifpaf -e VAULT_TEST run vault -- python setup.py testr --slowest --testr-args='{posargs}'
|
||||||
|
|
||||||
[testenv:genconfig]
|
[testenv:genconfig]
|
||||||
commands =
|
commands =
|
||||||
oslo-config-generator --config-file=etc/castellan/functional-config-generator.conf
|
oslo-config-generator --config-file=etc/castellan/functional-config-generator.conf
|
||||||
|
Loading…
x
Reference in New Issue
Block a user