diff --git a/.gitignore b/.gitignore index e40d1ea..6404634 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ # Sphinx doc/build +releasenotes/build # pbr generates these AUTHORS diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 0000000..f54c741 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,58 @@ +- job: + name: castellan-functional-vault + parent: openstack-tox-py27 + description: | + Run tox functional-vault target + required-projects: + - name: openstack/castellan + vars: + tox_envlist: functional-vault + +- job: + name: castellan-functional-devstack + parent: devstack + description: | + Run DevStack-based Castellan functional tests + pre-run: playbooks/devstack/pre.yaml + run: playbooks/devstack/run.yaml + post-run: playbooks/devstack/post.yaml + required-projects: + - name: openstack/castellan + - name: openstack/barbican + - name: openstack/python-barbicanclient + roles: + - zuul: openstack-infra/devstack + timeout: 9000 + vars: + devstack_services: + # is there a way to disable all services? I only want barbican + ceilometer-acentral: False + ceilometer-acompute: False + ceilometer-alarm-evaluator: False + ceilometer-alarm-notifier: False + ceilometer-anotification: False + ceilometer-api: False + ceilometer-collector: False + horizon: False + s-account: False + s-container: False + s-object: False + s-proxy: False + devstack_plugins: + barbican: git://git.openstack.org/openstack/barbican + tox_environment: + PYTHONUNBUFFERED: 'true' + tox_install_siblings: False # I don't know what this means + tox_envlist: functional + zuul_work_dir: src/git.openstack.org/openstack/castellan + +- project: + name: openstack/castellan + check: + jobs: + - castellan-functional-vault + - castellan-functional-devstack + gate: + jobs: + - castellan-functional-vault + - castellan-functional-devstack diff --git a/castellan/common/credentials/keystone_password.py b/castellan/common/credentials/keystone_password.py index 2351740..2e3ef7f 100644 --- a/castellan/common/credentials/keystone_password.py +++ b/castellan/common/credentials/keystone_password.py @@ -23,13 +23,14 @@ class KeystonePassword(password.Password): """This class represents a keystone password credential.""" - def __init__(self, password, username=None, user_id=None, + def __init__(self, password, auth_url=None, username=None, user_id=None, user_domain_id=None, user_domain_name=None, trust_id=None, domain_id=None, domain_name=None, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None, reauthenticate=True): """Create a new Keystone Password Credential. + :param string auth_url: Use this endpoint to connect to Keystone. :param string password: Password for authentication. :param string username: Username for authentication. :param string user_id: User ID for authentication. @@ -46,6 +47,7 @@ one is going to expire. (optional) default True """ + self._auth_url = auth_url self._user_id = user_id self._user_domain_id = user_domain_id self._user_domain_name = user_domain_name @@ -60,6 +62,11 @@ super(KeystonePassword, self).__init__(username, password) + + @property + def auth_url(self): + """This method returns an auth_url.""" + return self._auth_url @property def user_id(self): diff --git a/castellan/common/credentials/keystone_token.py b/castellan/common/credentials/keystone_token.py index 870895f..26d8bad 100644 --- a/castellan/common/credentials/keystone_token.py +++ b/castellan/common/credentials/keystone_token.py @@ -23,13 +23,15 @@ class KeystoneToken(token.Token): """This class represents a keystone token credential.""" - def __init__(self, token, trust_id=None, domain_id=None, domain_name=None, - project_id=None, project_name=None, project_domain_id=None, - project_domain_name=None, reauthenticate=True): + def __init__(self, token, auth_url=None, trust_id=None, domain_id=None, + domain_name=None, project_id=None, project_name=None, + project_domain_id=None, project_domain_name=None, + reauthenticate=True): """Create a new Keystone Token Credential. :param string token: Token for authentication. The type of token formats accepted are UUID, PKI, and Fernet. + :param string auth_url: Use this endpoint to connect to Keystone. :param string trust_id: Trust ID for trust scoping. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. @@ -41,6 +43,7 @@ one is going to expire. (optional) default True """ + self._auth_url = auth_url self._trust_id = trust_id self._domain_id = domain_id self._domain_name = domain_name @@ -51,6 +54,11 @@ self._reauthenticate = reauthenticate super(KeystoneToken, self).__init__(token) + + @property + def auth_url(self): + """This method returns an auth_url.""" + return self._auth_url @property def trust_id(self): diff --git a/castellan/common/objects/managed_object.py b/castellan/common/objects/managed_object.py index 58d0589..de3c2e7 100644 --- a/castellan/common/objects/managed_object.py +++ b/castellan/common/objects/managed_object.py @@ -28,11 +28,12 @@ class ManagedObject(object): """Base class to represent all managed objects.""" - def __init__(self, name=None, created=None): + def __init__(self, name=None, created=None, id=None): """Managed Object :param name: the name of the managed object. :param created: the time a managed object was created. + :param id: the ID of the object, generated after storing the object. """ self._name = name @@ -42,6 +43,17 @@ else: raise ValueError('created must be of long type, actual type %s' % type(created)) + + self._id = id + + @property + def id(self): + """Returns the ID of the managed object. + + Returns the ID of the managed object or None if this object does not + have one. If the ID is None, the object has not been persisted yet. + """ + return self._id @property def name(self): diff --git a/castellan/common/objects/opaque_data.py b/castellan/common/objects/opaque_data.py index 81b8e45..9512ba2 100644 --- a/castellan/common/objects/opaque_data.py +++ b/castellan/common/objects/opaque_data.py @@ -25,13 +25,13 @@ class OpaqueData(managed_object.ManagedObject): """This class represents opaque data.""" - def __init__(self, data, name=None, created=None): + def __init__(self, data, name=None, created=None, id=None): """Create a new OpaqueData object. Expected type for data is a bytestring. """ self._data = data - super(OpaqueData, self).__init__(name=name, created=created) + super(OpaqueData, self).__init__(name=name, created=created, id=id) @property def format(self): diff --git a/castellan/common/objects/passphrase.py b/castellan/common/objects/passphrase.py index 346d8f6..e0441b5 100644 --- a/castellan/common/objects/passphrase.py +++ b/castellan/common/objects/passphrase.py @@ -25,13 +25,13 @@ class Passphrase(managed_object.ManagedObject): """This class represents a passphrase.""" - def __init__(self, passphrase, name=None, created=None): + def __init__(self, passphrase, name=None, created=None, id=None): """Create a new Passphrase object. The expected type for the passphrase is a bytestring. """ self._passphrase = passphrase - super(Passphrase, self).__init__(name=name, created=created) + super(Passphrase, self).__init__(name=name, created=created, id=id) @property def format(self): diff --git a/castellan/common/objects/private_key.py b/castellan/common/objects/private_key.py index 45d936a..6472ef8 100644 --- a/castellan/common/objects/private_key.py +++ b/castellan/common/objects/private_key.py @@ -26,7 +26,7 @@ """This class represents private keys.""" def __init__(self, algorithm, bit_length, key, - name=None, created=None): + name=None, created=None, id=None): """Create a new PrivateKey object. The arguments specify the algorithm and bit length for the asymmetric @@ -35,7 +35,7 @@ self._alg = algorithm self._bit_length = bit_length self._key = key - super(PrivateKey, self).__init__(name=name, created=created) + super(PrivateKey, self).__init__(name=name, created=created, id=id) @property def algorithm(self): diff --git a/castellan/common/objects/public_key.py b/castellan/common/objects/public_key.py index 6d11eb3..efcc6b5 100644 --- a/castellan/common/objects/public_key.py +++ b/castellan/common/objects/public_key.py @@ -26,7 +26,7 @@ """This class represents public keys.""" def __init__(self, algorithm, bit_length, key, - name=None, created=None): + name=None, created=None, id=None): """Create a new PublicKey object. The arguments specify the algorithm and bit length for the asymmetric @@ -36,7 +36,7 @@ self._alg = algorithm self._bit_length = bit_length self._key = key - super(PublicKey, self).__init__(name=name, created=created) + super(PublicKey, self).__init__(name=name, created=created, id=id) @property def algorithm(self): diff --git a/castellan/common/objects/symmetric_key.py b/castellan/common/objects/symmetric_key.py index 499c159..f9cefeb 100644 --- a/castellan/common/objects/symmetric_key.py +++ b/castellan/common/objects/symmetric_key.py @@ -26,7 +26,7 @@ """This class represents symmetric keys.""" def __init__(self, algorithm, bit_length, key, - name=None, created=None): + name=None, created=None, id=None): """Create a new SymmetricKey object. The arguments specify the algorithm and bit length for the symmetric @@ -35,7 +35,7 @@ self._alg = algorithm self._bit_length = bit_length self._key = key - super(SymmetricKey, self).__init__(name=name, created=created) + super(SymmetricKey, self).__init__(name=name, created=created, id=id) @property def algorithm(self): diff --git a/castellan/common/objects/x_509.py b/castellan/common/objects/x_509.py index 8eaa1c9..aba1a88 100644 --- a/castellan/common/objects/x_509.py +++ b/castellan/common/objects/x_509.py @@ -25,13 +25,13 @@ class X509(certificate.Certificate): """This class represents X.509 certificates.""" - def __init__(self, data, name=None, created=None): + def __init__(self, data, name=None, created=None, id=None): """Create a new X509 object. The data should be in a bytestring. """ self._data = data - super(X509, self).__init__(name=name, created=created) + super(X509, self).__init__(name=name, created=created, id=id) @property def format(self): diff --git a/castellan/common/utils.py b/castellan/common/utils.py index a040c96..f6ba646 100644 --- a/castellan/common/utils.py +++ b/castellan/common/utils.py @@ -51,6 +51,8 @@ "'keystone_password' auth_type."), # keystone credential opts + cfg.StrOpt('auth_url', + help="Use this endpoint to connect to Keystone."), cfg.StrOpt('user_id', help="User ID for authentication. Optional for " "'keystone_token' and 'keystone_password' auth_type."), @@ -130,6 +132,7 @@ elif conf.key_manager.auth_type == 'keystone_password': return keystone_password.KeystonePassword( conf.key_manager.password, + auth_url=conf.key_manager.auth_url, username=conf.key_manager.username, user_id=conf.key_manager.user_id, user_domain_id=conf.key_manager.user_domain_id, @@ -153,6 +156,7 @@ return keystone_token.KeystoneToken( auth_token, + auth_url=conf.key_manager.auth_url, trust_id=conf.key_manager.trust_id, domain_id=conf.key_manager.domain_id, domain_name=conf.key_manager.domain_name, diff --git a/castellan/key_manager/__init__.py b/castellan/key_manager/__init__.py index 0a7435a..16e23cd 100644 --- a/castellan/key_manager/__init__.py +++ b/castellan/key_manager/__init__.py @@ -12,14 +12,24 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from castellan.key_manager import migration from oslo_config import cfg +from oslo_log import log as logging from oslo_utils import importutils +from stevedore import driver +from stevedore import exception + +LOG = logging.getLogger(__name__) key_manager_opts = [ - cfg.StrOpt('api_class', - default='castellan.key_manager.barbican_key_manager' - '.BarbicanKeyManager', - help='The full class name of the key manager API class'), + cfg.StrOpt('backend', + default='barbican', + deprecated_name='api_class', + deprecated_group='key_manager', + help='Specify the key manager implementation. Options are ' + '"barbican" and "vault". Default is "barbican". Will ' + 'support the values earlier set using ' + '[key_manager]/api_class for some time.'), ] @@ -27,5 +37,16 @@ conf = configuration or cfg.CONF conf.register_opts(key_manager_opts, group='key_manager') - cls = importutils.import_class(conf.key_manager.api_class) - return cls(configuration=conf) + try: + mgr = driver.DriverManager("castellan.drivers", + conf.key_manager.backend, + invoke_on_load=True, + invoke_args=[conf]) + key_mgr = mgr.driver + except exception.NoMatches: + LOG.warning("Deprecation Warning : %s is not a stevedore based driver," + " trying to load it as a class", conf.key_manager.backend) + cls = importutils.import_class(conf.key_manager.backend) + key_mgr = cls(configuration=conf) + + return migration.handle_migration(conf, key_mgr) diff --git a/castellan/key_manager/barbican_key_manager.py b/castellan/key_manager/barbican_key_manager.py index d73fc32..0a4bdd5 100644 --- a/castellan/key_manager/barbican_key_manager.py +++ b/castellan/key_manager/barbican_key_manager.py @@ -41,7 +41,7 @@ from castellan.key_manager import key_manager -from barbicanclient import client as barbican_client +from barbicanclient import client as barbican_client_import from barbicanclient import exceptions as barbican_exceptions from oslo_utils import timeutils from six.moves import urllib @@ -55,6 +55,8 @@ help='Version of the Barbican API, for example: "v1"'), cfg.StrOpt('auth_endpoint', default='http://localhost/identity/v3', + deprecated_name='auth_url', + deprecated_group='key_manager', help='Use this endpoint to connect to Keystone'), cfg.IntOpt('retry_delay', default=1, @@ -118,11 +120,13 @@ verify=self.conf.barbican.verify_ssl) self._barbican_endpoint = self._get_barbican_endpoint(auth, sess) - self._barbican_client = barbican_client.Client( + self._barbican_client = barbican_client_import.Client( session=sess, endpoint=self._barbican_endpoint) self._current_context = context + # TODO(pbourke): more fine grained exception handling - we are eating + # tracebacks here except Exception as e: LOG.error("Error creating Barbican client: %s", e) raise exception.KeyManagerError(reason=e) @@ -134,11 +138,9 @@ return self._barbican_client def _get_keystone_auth(self, context): - auth_url = self.conf.barbican.auth_endpoint - if context.__class__.__name__ is 'KeystonePassword': return identity.Password( - auth_url=auth_url, + auth_url=context.auth_url, username=context.username, password=context.password, user_id=context.user_id, @@ -154,7 +156,7 @@ reauthenticate=context.reauthenticate) elif context.__class__.__name__ is 'KeystoneToken': return identity.Token( - auth_url=auth_url, + auth_url=context.auth_url, token=context.token, trust_id=context.trust_id, domain_id=context.domain_id, @@ -168,9 +170,12 @@ # projects begin to use utils.credential_factory elif context.__class__.__name__ is 'RequestContext': return identity.Token( - auth_url=auth_url, + auth_url=self.conf.barbican.auth_endpoint, token=context.auth_token, - project_id=context.tenant) + project_id=context.project_id, + project_name=context.project_name, + project_domain_id=context.project_domain_id, + project_domain_name=context.project_domain_name) else: msg = _("context must be of type KeystonePassword, " "KeystoneToken, or RequestContext.") @@ -200,8 +205,12 @@ latest_version = raw_data[-1] api_version = latest_version.get('id') + if endpoint[-1] != '/': + endpoint += '/' + base_url = urllib.parse.urljoin( endpoint, api_version) + return base_url def create_key(self, context, algorithm, length, @@ -478,6 +487,11 @@ else: secret_data = self._get_secret_data(secret) + if secret.secret_ref: + object_id = self._retrieve_secret_uuid(secret.secret_ref) + else: + object_id = None + # convert created ISO8601 in Barbican to POSIX if secret.created: time_stamp = timeutils.parse_isotime( @@ -489,11 +503,13 @@ secret.bit_length, secret_data, secret.name, - created) + created, + object_id) else: return secret_type(secret_data, secret.name, - created) + created, + object_id) def _get_secret(self, context, object_id): """Returns the metadata of the secret. @@ -572,3 +588,44 @@ uuid=managed_object_id) else: raise exception.KeyManagerError(reason=e) + + def list(self, context, object_type=None, metadata_only=False): + """Retrieves a list of managed objects that match the criteria. + + If no search criteria is given, all objects are returned. + + :param context: contains information of the user and the environment + for the request (castellan/context.py) + :param object_type: the type of object to retrieve + :param metadata_only: whether secret data should be included + :raises KeyManagerError: if listing secrets fails + """ + objects = [] + barbican_client = self._get_barbican_client(context) + + if object_type and object_type not in self._secret_type_dict: + msg = _("Invalid secret type: %s") % object_type + LOG.error(msg) + raise exception.KeyManagerError(reason=msg) + + secret_type = self._secret_type_dict.get(object_type) + + try: + secrets = barbican_client.secrets.list(secret_type=secret_type) + except (barbican_exceptions.HTTPAuthError, + barbican_exceptions.HTTPClientError, + barbican_exceptions.HTTPServerError) as e: + LOG.error(_("Error listing objects: %s"), e) + raise exception.KeyManagerError(reason=e) + + for secret in secrets: + try: + obj = self._get_castellan_object(secret, metadata_only) + objects.append(obj) + except (barbican_exceptions.HTTPAuthError, + barbican_exceptions.HTTPClientError, + barbican_exceptions.HTTPServerError) as e: + LOG.warning(_("Error occurred while retrieving object " + "metadata, not adding it to the list: %s"), e) + + return objects diff --git a/castellan/key_manager/key_manager.py b/castellan/key_manager/key_manager.py index 6ec840f..07b8829 100644 --- a/castellan/key_manager/key_manager.py +++ b/castellan/key_manager/key_manager.py @@ -109,3 +109,17 @@ considered "non-existent" and completely invisible. """ pass + + def list(self, context, object_type=None, metadata_only=False): + """Lists the managed objects given the criteria. + + Implementations should verify that the caller has permission to list + the managed objects and should only list the objects the caller has + access to by checking the context object (context). A NotAuthorized + exception should be raised if the caller lacks permission. + + A list of managed objects or managed object metadata should be + returned, depending on the metadata_only flag. If no objects are + found, an empty list should be returned instead. + """ + return [] diff --git a/castellan/key_manager/migration.py b/castellan/key_manager/migration.py new file mode 100644 index 0000000..324d111 --- /dev/null +++ b/castellan/key_manager/migration.py @@ -0,0 +1,72 @@ +# Copyright 2017 Red Hat, Inc. +# All Rights Reserved. +# +# 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. +import binascii +from castellan.common import exception +from castellan.common.objects import symmetric_key +from oslo_config import cfg +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +def handle_migration(conf, key_mgr): + try: + conf.register_opt(cfg.StrOpt('fixed_key'), group='key_manager') + except cfg.DuplicateOptError: + pass + + if conf.key_manager.fixed_key is not None and \ + not conf.key_manager.backend.endswith('ConfKeyManager'): + + LOG.warning("Using MigrationKeyManager to provide support for legacy" + " fixed_key encryption") + + class MigrationKeyManager(type(key_mgr)): + def __init__(self, configuration): + self.fixed_key = configuration.key_manager.fixed_key + self.fixed_key_id = '00000000-0000-0000-0000-000000000000' + super(MigrationKeyManager, self).__init__(configuration) + + def get(self, context, managed_object_id): + if managed_object_id == self.fixed_key_id: + LOG.debug("Processing request for secret associated" + " with fixed_key key ID") + + if context is None: + raise exception.Forbidden() + + key_bytes = bytes(binascii.unhexlify(self.fixed_key)) + secret = symmetric_key.SymmetricKey('AES', + len(key_bytes) * 8, + key_bytes) + else: + secret = super(MigrationKeyManager, self).get( + context, managed_object_id) + return secret + + def delete(self, context, managed_object_id): + if managed_object_id == self.fixed_key_id: + LOG.debug("Not deleting key associated with" + " fixed_key key ID") + + if context is None: + raise exception.Forbidden() + else: + super(MigrationKeyManager, self).delete(context, + managed_object_id) + + key_mgr = MigrationKeyManager(configuration=conf) + + return key_mgr diff --git a/castellan/key_manager/not_implemented_key_manager.py b/castellan/key_manager/not_implemented_key_manager.py index b4f32ad..57eff3c 100644 --- a/castellan/key_manager/not_implemented_key_manager.py +++ b/castellan/key_manager/not_implemented_key_manager.py @@ -45,5 +45,8 @@ def get(self, context, managed_object_id, **kwargs): raise NotImplementedError() + def list(self, context, object_type=None): + raise NotImplementedError() + def delete(self, context, managed_object_id, **kwargs): raise NotImplementedError() diff --git a/castellan/key_manager/vault_key_manager.py b/castellan/key_manager/vault_key_manager.py new file mode 100644 index 0000000..6575ade --- /dev/null +++ b/castellan/key_manager/vault_key_manager.py @@ -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 diff --git a/castellan/options.py b/castellan/options.py index ee4165f..e6bc245 100644 --- a/castellan/options.py +++ b/castellan/options.py @@ -20,6 +20,12 @@ from castellan.key_manager import barbican_key_manager as bkm except ImportError: bkm = None + +try: + from castellan.key_manager import vault_key_manager as vkm +except ImportError: + vkm = None + from castellan.common import utils _DEFAULT_LOG_LEVELS = ['castellan=WARN'] @@ -30,9 +36,11 @@ '%(message)s') -def set_defaults(conf, api_class=None, barbican_endpoint=None, +def set_defaults(conf, backend=None, barbican_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, vault_root_token_id=None, vault_url=None, + vault_ssl_ca_crt_file=None, vault_use_ssl=None): """Set defaults for configuration values. Overrides the default options values. @@ -44,13 +52,21 @@ :param retry_delay: Use this attribute to set retry delay. :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 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') if bkm: conf.register_opts(bkm.barbican_opts, group=bkm.BARBICAN_OPT_GROUP) + if vkm: + conf.register_opts(vkm.vault_opts, group=vkm.VAULT_OPT_GROUP) - if api_class is not None: - conf.set_default('api_class', api_class, group='key_manager') + # Use the new backend option if set or fall back to the older api_class + default_backend = backend or api_class + if default_backend is not None: + conf.set_default('backend', default_backend, group='key_manager') if bkm is not None: if barbican_endpoint is not None: @@ -71,6 +87,20 @@ if verify_ssl is not None: conf.set_default('verify_ssl', verify_ssl, 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'): @@ -106,4 +136,6 @@ if bkm is not None: 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 diff --git a/castellan/tests/contrib/post_test_hook.sh b/castellan/tests/contrib/post_test_hook.sh index 1b09526..eb83d74 100755 --- a/castellan/tests/contrib/post_test_hook.sh +++ b/castellan/tests/contrib/post_test_hook.sh @@ -12,7 +12,7 @@ sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html sudo gzip -9 $BASE/logs/testrepository.subunit sudo gzip -9 $BASE/logs/testr_results.html - sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + sudo chown $USER:$USER $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz fi } @@ -24,14 +24,6 @@ sudo chown -R $owner:stack $CASTELLAN_DIR testenv=functional - -sudo -H -u $owner tox -e genconfig - -if [ ! -d /etc/castellan ]; then - sudo mkdir /etc/castellan -fi - -sudo cp $CASTELLAN_DIR/etc/castellan/castellan-functional.conf.sample /etc/castellan/castellan-functional.conf # Run tests echo "Running Castellan $testenv test suite" diff --git a/castellan/tests/functional/key_manager/test_barbican_key_manager.py b/castellan/tests/functional/key_manager/test_barbican_key_manager.py index 8fed35c..5fdd1a3 100644 --- a/castellan/tests/functional/key_manager/test_barbican_key_manager.py +++ b/castellan/tests/functional/key_manager/test_barbican_key_manager.py @@ -26,6 +26,7 @@ from oslo_context import context from oslo_utils import uuidutils from oslotest import base +from testtools import testcase from castellan.common.credentials import keystone_password from castellan.common.credentials import keystone_token @@ -50,7 +51,13 @@ def setUp(self): super(BarbicanKeyManagerTestCase, self).setUp() - self.ctxt = self.get_context() + try: + 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): super(BarbicanKeyManagerTestCase, self).tearDown() @@ -129,6 +136,7 @@ base.BaseTestCase): def get_context(self): + auth_url = CONF.identity.auth_url username = CONF.identity.username password = CONF.identity.password project_name = CONF.identity.project_name @@ -136,7 +144,7 @@ project_domain_name = CONF.identity.project_domain_name ctxt = keystone_password.KeystonePassword( - username=username, password=password, + auth_url=auth_url, username=username, password=password, project_name=project_name, user_domain_name=user_domain_name, project_domain_name=project_domain_name) @@ -165,4 +173,5 @@ return keystone_token.KeystoneToken( token=auth.get_token(sess), + auth_url=auth_url, project_id=auth.get_project_id(sess)) diff --git a/castellan/tests/functional/key_manager/test_key_manager.py b/castellan/tests/functional/key_manager/test_key_manager.py index 85ff73e..1f8eaf6 100644 --- a/castellan/tests/functional/key_manager/test_key_manager.py +++ b/castellan/tests/functional/key_manager/test_key_manager.py @@ -137,6 +137,8 @@ self.assertEqual(managed_object.get_encoded(), retrieved_object.get_encoded()) self.assertFalse(managed_object.is_metadata_only()) + self.assertFalse(retrieved_object.is_metadata_only()) + self.assertIsNotNone(retrieved_object.id) @utils.parameterized_dataset({ 'symmetric_key': [_get_test_symmetric_key()], @@ -155,6 +157,7 @@ metadata_only=True) self.assertFalse(managed_object.is_metadata_only()) self.assertTrue(retrieved_object.is_metadata_only()) + self.assertIsNotNone(retrieved_object.id) @utils.parameterized_dataset({ 'symmetric_key': [_get_test_symmetric_key()], @@ -171,3 +174,70 @@ retrieved_object = self.key_mgr.get(self.ctxt, uuid) self.assertEqual(managed_object.get_encoded(), retrieved_object.get_encoded()) + self.assertIsNotNone(retrieved_object.id) + + @utils.parameterized_dataset({ + 'symmetric_key': [_get_test_symmetric_key()], + 'public_key': [_get_test_public_key()], + 'private_key': [_get_test_private_key()], + 'certificate': [_get_test_certificate()], + 'passphrase': [_get_test_passphrase()], + 'opaque_data': [_get_test_opaque_data()], + }) + def test_list(self, managed_object): + uuid = self.key_mgr.store(self.ctxt, managed_object) + self.addCleanup(self.key_mgr.delete, self.ctxt, uuid) + + # the list command may return more objects than the one we just + # created if older objects were not cleaned up, so we will simply + # check if the object we created is in the list + retrieved_objects = self.key_mgr.list(self.ctxt) + self.assertTrue(managed_object in retrieved_objects) + for retrieved_object in retrieved_objects: + self.assertFalse(retrieved_object.is_metadata_only()) + self.assertIsNotNone(retrieved_object.id) + + @utils.parameterized_dataset({ + 'symmetric_key': [_get_test_symmetric_key()], + 'public_key': [_get_test_public_key()], + 'private_key': [_get_test_private_key()], + 'certificate': [_get_test_certificate()], + 'passphrase': [_get_test_passphrase()], + 'opaque_data': [_get_test_opaque_data()], + }) + def test_list_metadata_only(self, managed_object): + uuid = self.key_mgr.store(self.ctxt, managed_object) + self.addCleanup(self.key_mgr.delete, self.ctxt, uuid) + + expected_obj = self.key_mgr.get(self.ctxt, uuid, metadata_only=True) + + # the list command may return more objects than the one we just + # created if older objects were not cleaned up, so we will simply + # check if the object we created is in the list + retrieved_objects = self.key_mgr.list(self.ctxt, metadata_only=True) + self.assertTrue(expected_obj in retrieved_objects) + for retrieved_object in retrieved_objects: + self.assertTrue(retrieved_object.is_metadata_only()) + self.assertIsNotNone(retrieved_object.id) + + @utils.parameterized_dataset({ + 'query_by_object_type': { + 'object_1': _get_test_symmetric_key(), + 'object_2': _get_test_public_key(), + 'query_dict': dict(object_type=symmetric_key.SymmetricKey) + }, + }) + def test_list_with_filter(self, object_1, object_2, query_dict): + uuid1 = self.key_mgr.store(self.ctxt, object_1) + uuid2 = self.key_mgr.store(self.ctxt, object_2) + self.addCleanup(self.key_mgr.delete, self.ctxt, uuid1) + self.addCleanup(self.key_mgr.delete, self.ctxt, uuid2) + + # the list command may return more objects than the one we just + # created if older objects were not cleaned up, so we will simply + # check that the returned objects have the expected type + retrieved_objects = self.key_mgr.list(self.ctxt, **query_dict) + for retrieved_object in retrieved_objects: + self.assertEqual(type(object_1), type(retrieved_object)) + self.assertIsNotNone(retrieved_object.id) + self.assertTrue(object_1 in retrieved_objects) diff --git a/castellan/tests/functional/key_manager/test_vault_key_manager.py b/castellan/tests/functional/key_manager/test_vault_key_manager.py new file mode 100644 index 0000000..e7e027b --- /dev/null +++ b/castellan/tests/functional/key_manager/test_vault_key_manager.py @@ -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() diff --git a/castellan/tests/unit/key_manager/mock_key_manager.py b/castellan/tests/unit/key_manager/mock_key_manager.py index 384a4b6..5fee24b 100644 --- a/castellan/tests/unit/key_manager/mock_key_manager.py +++ b/castellan/tests/unit/key_manager/mock_key_manager.py @@ -163,6 +163,7 @@ raise exception.Forbidden() key_id = self._generate_key_id() + managed_object._id = key_id self.keys[key_id] = managed_object return key_id @@ -226,3 +227,19 @@ random.shuffle(password) return ''.join(password) + + def list(self, context, object_type=None, metadata_only=False): + """Retrieves a list of managed objects that match the criteria. + + A Forbidden exception is raised if the context is None. + If no search criteria is given, all objects are returned. + """ + if context is None: + raise exception.Forbidden() + + objects = [] + for obj_id in self.keys: + obj = self.get(context, obj_id, metadata_only=metadata_only) + if type(obj) == object_type or object_type is None: + objects.append(obj) + return objects diff --git a/castellan/tests/unit/key_manager/test_barbican_key_manager.py b/castellan/tests/unit/key_manager/test_barbican_key_manager.py index 97a6155..d4fe682 100644 --- a/castellan/tests/unit/key_manager/test_barbican_key_manager.py +++ b/castellan/tests/unit/key_manager/test_barbican_key_manager.py @@ -20,7 +20,6 @@ from barbicanclient import exceptions as barbican_exceptions import mock -from oslo_config import cfg from oslo_utils import timeutils from castellan.common import exception @@ -32,7 +31,7 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase): def _create_key_manager(self): - return barbican_key_manager.BarbicanKeyManager(cfg.CONF) + return barbican_key_manager.BarbicanKeyManager(self.conf) def setUp(self): super(BarbicanKeyManagerTestCase, self).setUp() @@ -72,9 +71,28 @@ self.delete = self.mock_barbican.secrets.delete self.store = self.mock_barbican.secrets.store self.create = self.mock_barbican.secrets.create + self.list = self.mock_barbican.secrets.list self.key_mgr._barbican_client = self.mock_barbican self.key_mgr._current_context = self.ctxt + + def test_base_url_old_version(self): + version = "v1" + self.key_mgr.conf.barbican.barbican_api_version = version + endpoint = "http://localhost:9311" + base_url = self.key_mgr._create_base_url(mock.Mock(), + mock.Mock(), + endpoint) + self.assertEqual(endpoint + "/" + version, base_url) + + def test_base_url_new_version(self): + version = "v1" + self.key_mgr.conf.barbican.barbican_api_version = version + endpoint = "http://localhost/key_manager" + base_url = self.key_mgr._create_base_url(mock.Mock(), + mock.Mock(), + endpoint) + self.assertEqual(endpoint + "/" + version, base_url) def test_create_key(self): # Create order_ref_url and assign return value @@ -189,6 +207,10 @@ original_secret_metadata.bit_length = mock.sentinel.bit original_secret_metadata.secret_type = 'symmetric' + key_id = "43ed09c3-e551-4c24-b612-e619abe9b534" + key_ref = ("http://localhost:9311/v1/secrets/" + key_id) + original_secret_metadata.secret_ref = key_ref + created = timeutils.parse_isotime('2015-10-20 18:51:17+00:00') original_secret_metadata.created = created created_formatted = timeutils.parse_isotime(str(created)) @@ -204,6 +226,7 @@ key = self.key_mgr.get(self.ctxt, self.key_id) self.get.assert_called_once_with(self.secret_ref) + self.assertEqual(key_id, key.id) self.assertEqual(key_name, key.name) self.assertEqual(original_secret_data, key.get_encoded()) self.assertEqual(created_posix, key.created) @@ -348,3 +371,65 @@ order_ref_url) self.assertEqual(1, self.mock_barbican.orders.get.call_count) + + def test_list_null_context(self): + self.key_mgr._barbican_client = None + self.assertRaises(exception.Forbidden, + self.key_mgr.list, None) + + def test_list(self): + original_secret_metadata = mock.Mock() + original_secret_metadata.algorithm = mock.sentinel.alg + original_secret_metadata.bit_length = mock.sentinel.bit + original_secret_metadata.secret_type = 'symmetric' + + key_id = "43ed09c3-e551-4c24-b612-e619abe9b534" + key_ref = ("http://localhost:9311/v1/secrets/" + key_id) + original_secret_metadata.secret_ref = key_ref + + created = timeutils.parse_isotime('2015-10-20 18:51:17+00:00') + original_secret_metadata.created = created + created_formatted = timeutils.parse_isotime(str(created)) + created_posix = calendar.timegm(created_formatted.timetuple()) + + key_name = 'my key' + original_secret_metadata.name = key_name + + original_secret_data = b'test key' + original_secret_metadata.payload = original_secret_data + + self.mock_barbican.secrets.list.return_value = ( + [original_secret_metadata]) + + # check metadata_only = False + key_list = self.key_mgr.list(self.ctxt) + self.assertEqual(1, len(key_list)) + key = key_list[0] + + self.list.assert_called_once() + self.assertEqual(key_id, key.id) + self.assertEqual(key_name, key.name) + self.assertEqual(original_secret_data, key.get_encoded()) + self.assertEqual(created_posix, key.created) + + self.list.reset_mock() + + # check metadata_only = True + key_list = self.key_mgr.list(self.ctxt, metadata_only=True) + self.assertEqual(1, len(key_list)) + key = key_list[0] + + self.list.assert_called_once() + self.assertEqual(key_name, key.name) + self.assertIsNone(key.get_encoded()) + self.assertEqual(created_posix, key.created) + + def test_list_with_error(self): + self.mock_barbican.secrets.list = mock.Mock( + side_effect=barbican_exceptions.HTTPClientError('test error')) + self.assertRaises(exception.KeyManagerError, + self.key_mgr.list, self.ctxt) + + def test_list_with_invalid_object_type(self): + self.assertRaises(exception.KeyManagerError, + self.key_mgr.list, self.ctxt, "invalid_type") diff --git a/castellan/tests/unit/key_manager/test_key_manager.py b/castellan/tests/unit/key_manager/test_key_manager.py index 5b29b1d..926e91a 100644 --- a/castellan/tests/unit/key_manager/test_key_manager.py +++ b/castellan/tests/unit/key_manager/test_key_manager.py @@ -17,7 +17,14 @@ Test cases for the key manager. """ +from oslo_config import cfg +from oslo_config import fixture + +from castellan import key_manager +from castellan.key_manager import barbican_key_manager from castellan.tests import base + +CONF = cfg.CONF class KeyManagerTestCase(base.TestCase): @@ -28,4 +35,18 @@ def setUp(self): super(KeyManagerTestCase, self).setUp() + self.conf = self.useFixture(fixture.Config()).conf + self.key_mgr = self._create_key_manager() + + +class DefaultKeyManagerImplTestCase(KeyManagerTestCase): + + def _create_key_manager(self): + return key_manager.API(self.conf) + + def test_default_key_manager(self): + self.assertEqual("barbican", self.conf.key_manager.backend) + self.assertIsNotNone(self.key_mgr) + self.assertIsInstance(self.key_mgr, + barbican_key_manager.BarbicanKeyManager) diff --git a/castellan/tests/unit/key_manager/test_migration_key_manager.py b/castellan/tests/unit/key_manager/test_migration_key_manager.py new file mode 100644 index 0000000..e5c8ba6 --- /dev/null +++ b/castellan/tests/unit/key_manager/test_migration_key_manager.py @@ -0,0 +1,125 @@ +# Copyright 2017 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +""" +Test cases for the migration key manager. +""" + +import binascii +import mock + +from oslo_config import cfg + +from castellan.common import exception +from castellan.common.objects import symmetric_key as key +from castellan import key_manager +from castellan.key_manager import not_implemented_key_manager +from castellan.tests.unit.key_manager import test_key_manager + +CONF = cfg.CONF + + +class ConfKeyManager(not_implemented_key_manager.NotImplementedKeyManager): + pass + + +class MigrationKeyManagerTestCase(test_key_manager.KeyManagerTestCase): + + def _create_key_manager(self): + self.fixed_key = '1' * 64 + try: + self.conf.register_opt(cfg.StrOpt('fixed_key'), + group='key_manager') + except cfg.DuplicateOptError: + pass + self.conf.set_override('fixed_key', + self.fixed_key, + group='key_manager') + return key_manager.API(self.conf) + + def setUp(self): + super(MigrationKeyManagerTestCase, self).setUp() + + # Create fake context (actual contents doesn't matter). + self.ctxt = mock.Mock() + + fixed_key_bytes = bytes(binascii.unhexlify(self.fixed_key)) + fixed_key_length = len(fixed_key_bytes) * 8 + self.fixed_key_secret = key.SymmetricKey('AES', + fixed_key_length, + fixed_key_bytes) + self.fixed_key_id = '00000000-0000-0000-0000-000000000000' + self.other_key_id = "d152fa13-2b41-42ca-a934-6c21566c0f40" + + def test_get_fixed_key(self): + self.assertEqual('MigrationKeyManager', type(self.key_mgr).__name__) + secret = self.key_mgr.get(self.ctxt, self.fixed_key_id) + self.assertEqual(self.fixed_key_secret, secret) + + def test_get_fixed_key_fail_bad_context(self): + self.assertRaises(exception.Forbidden, + self.key_mgr.get, + context=None, + managed_object_id=self.fixed_key_id) + + def test_delete_fixed_key(self): + self.key_mgr.delete(self.ctxt, self.fixed_key_id) + # Delete looks like it succeeded, but nothing actually happened. + secret = self.key_mgr.get(self.ctxt, self.fixed_key_id) + self.assertEqual(self.fixed_key_secret, secret) + + def test_delete_fixed_key_fail_bad_context(self): + self.assertRaises(exception.Forbidden, + self.key_mgr.delete, + context=None, + managed_object_id=self.fixed_key_id) + + def test_get_other_key(self): + # Request to get other_key_id should be passed on to the backend, + # who will throw an error because we don't have a valid context. + self.assertRaises(exception.KeyManagerError, + self.key_mgr.get, + context=self.ctxt, + managed_object_id=self.other_key_id) + + def test_delete_other_key(self): + # Request to delete other_key_id should be passed on to the backend, + # who will throw an error because we don't have a valid context. + self.assertRaises(exception.KeyManagerError, + self.key_mgr.delete, + context=self.ctxt, + managed_object_id=self.other_key_id) + + def test_no_fixed_key(self): + conf = self.conf + conf.set_override('fixed_key', None, group='key_manager') + key_mgr = key_manager.API(conf) + self.assertNotEqual('MigrationKeyManager', type(key_mgr).__name__) + self.assertRaises(exception.KeyManagerError, + key_mgr.get, + context=self.ctxt, + managed_object_id=self.fixed_key_id) + + def test_using_conf_key_manager(self): + conf = self.conf + ckm_backend = 'castellan.tests.unit.key_manager.' \ + 'test_migration_key_manager.ConfKeyManager' + conf.set_override('backend', ckm_backend, group='key_manager') + key_mgr = key_manager.API(conf) + self.assertNotEqual('MigrationKeyManager', type(key_mgr).__name__) + self.assertRaises(NotImplementedError, + key_mgr.get, + context=self.ctxt, + managed_object_id=self.fixed_key_id) diff --git a/castellan/tests/unit/key_manager/test_mock_key_manager.py b/castellan/tests/unit/key_manager/test_mock_key_manager.py index 4b0a40b..aef4155 100644 --- a/castellan/tests/unit/key_manager/test_mock_key_manager.py +++ b/castellan/tests/unit/key_manager/test_mock_key_manager.py @@ -54,6 +54,11 @@ self.context = context.RequestContext('fake', 'fake') + def cleanUp(self): + super(MockKeyManagerTestCase, self).cleanUp() + + self.key_mgr.keys = {} + def test_create_key(self): key_id_1 = self.key_mgr.create_key(self.context) key_id_2 = self.key_mgr.create_key(self.context) @@ -65,18 +70,21 @@ key_id = self.key_mgr.create_key(self.context, length=length) key = self.key_mgr.get(self.context, key_id) self.assertEqual(length / 8, len(key.get_encoded())) + self.assertIsNotNone(key.id) def test_create_key_with_name(self): name = 'my key' key_id = self.key_mgr.create_key(self.context, name=name) key = self.key_mgr.get(self.context, key_id) self.assertEqual(name, key.name) + self.assertIsNotNone(key.id) def test_create_key_with_algorithm(self): algorithm = 'DES' key_id = self.key_mgr.create_key(self.context, algorithm=algorithm) key = self.key_mgr.get(self.context, key_id) self.assertEqual(algorithm, key.algorithm) + self.assertIsNotNone(key.id) def test_create_key_null_context(self): self.assertRaises(exception.Forbidden, @@ -89,7 +97,9 @@ self.context, 'RSA', length, name=name) private_key = self.key_mgr.get(self.context, private_key_uuid) + self.assertIsNotNone(private_key.id) public_key = self.key_mgr.get(self.context, public_key_uuid) + self.assertIsNotNone(public_key.id) crypto_private_key = get_cryptography_private_key(private_key) crypto_public_key = get_cryptography_public_key(public_key) @@ -148,6 +158,8 @@ actual_key = self.key_mgr.get(self.context, key_id) self.assertEqual(_key, actual_key) + self.assertIsNotNone(actual_key.id) + def test_store_key_and_get_metadata(self): secret_key = bytes(b'0' * 64) _key = sym_key.SymmetricKey('AES', 64 * 8, secret_key) @@ -159,6 +171,8 @@ self.assertIsNone(actual_key.get_encoded()) self.assertTrue(actual_key.is_metadata_only()) + self.assertIsNotNone(actual_key.id) + def test_store_key_and_get_metadata_and_get_key(self): secret_key = bytes(b'0' * 64) _key = sym_key.SymmetricKey('AES', 64 * 8, secret_key) @@ -176,6 +190,8 @@ self.assertIsNotNone(actual_key.get_encoded()) self.assertFalse(actual_key.is_metadata_only()) + self.assertIsNotNone(actual_key.id) + def test_store_null_context(self): self.assertRaises(exception.Forbidden, self.key_mgr.store, None, None) @@ -201,3 +217,36 @@ def test_delete_unknown_key(self): self.assertRaises(KeyError, self.key_mgr.delete, self.context, None) + + def test_list_null_context(self): + self.assertRaises(exception.Forbidden, self.key_mgr.list, None) + + def test_list_keys(self): + key1 = sym_key.SymmetricKey('AES', 64 * 8, bytes(b'0' * 64)) + self.key_mgr.store(self.context, key1) + key2 = sym_key.SymmetricKey('AES', 32 * 8, bytes(b'0' * 32)) + self.key_mgr.store(self.context, key2) + + keys = self.key_mgr.list(self.context) + self.assertEqual(2, len(keys)) + self.assertTrue(key1 in keys) + self.assertTrue(key2 in keys) + + for key in keys: + self.assertIsNotNone(key.id) + + def test_list_keys_metadata_only(self): + key1 = sym_key.SymmetricKey('AES', 64 * 8, bytes(b'0' * 64)) + self.key_mgr.store(self.context, key1) + key2 = sym_key.SymmetricKey('AES', 32 * 8, bytes(b'0' * 32)) + self.key_mgr.store(self.context, key2) + + keys = self.key_mgr.list(self.context, metadata_only=True) + self.assertEqual(2, len(keys)) + bit_length_list = [key1.bit_length, key2.bit_length] + for key in keys: + self.assertTrue(key.is_metadata_only()) + self.assertTrue(key.bit_length in bit_length_list) + + for key in keys: + self.assertIsNotNone(key.id) diff --git a/castellan/tests/unit/key_manager/test_not_implemented_key_manager.py b/castellan/tests/unit/key_manager/test_not_implemented_key_manager.py index 1e439d6..c82d76e 100644 --- a/castellan/tests/unit/key_manager/test_not_implemented_key_manager.py +++ b/castellan/tests/unit/key_manager/test_not_implemented_key_manager.py @@ -46,6 +46,10 @@ self.assertRaises(NotImplementedError, self.key_mgr.get, None, None) + def test_list(self): + self.assertRaises(NotImplementedError, + self.key_mgr.list, None) + def test_delete(self): self.assertRaises(NotImplementedError, self.key_mgr.delete, None, None) diff --git a/castellan/tests/unit/test_options.py b/castellan/tests/unit/test_options.py index 280e4e6..e1ac3f3 100644 --- a/castellan/tests/unit/test_options.py +++ b/castellan/tests/unit/test_options.py @@ -15,9 +15,11 @@ from oslo_config import cfg +from castellan import key_manager from castellan.key_manager import barbican_key_manager as bkm from castellan import options from castellan.tests import base +from castellan.tests.unit.key_manager import mock_key_manager class TestOptions(base.TestCase): @@ -25,9 +27,15 @@ def test_set_defaults(self): conf = cfg.ConfigOpts() - api_class = 'test.api.class' - options.set_defaults(conf, api_class=api_class) - self.assertEqual(api_class, conf.key_manager.api_class) + self.assertTrue(isinstance(key_manager.API(conf), + bkm.BarbicanKeyManager)) + + cls = mock_key_manager.MockKeyManager + backend = '%s.%s' % (cls.__module__, cls.__name__) + options.set_defaults(conf, backend=backend) + self.assertEqual(backend, conf.key_manager.backend) + self.assertIsInstance(key_manager.API(conf), + mock_key_manager.MockKeyManager) barbican_endpoint = 'http://test-server.org:9311/' options.set_defaults(conf, barbican_endpoint=barbican_endpoint) diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess new file mode 100644 index 0000000..af736ab --- /dev/null +++ b/doc/source/_extra/.htaccess @@ -0,0 +1,3 @@ +redirectmatch 301 ^/castellan/([^/]+)/usage.html$ ^/user/index.html +redirectmatch 301 ^/castellan/([^/]+)/testing.html$ ^/contributor/testing.html +redirectmatch 301 ^/castellan/([^/]+)/installation.html$ ^/install/index.html diff --git a/doc/source/conf.py b/doc/source/conf.py index 6e2377f..9867895 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -64,6 +64,10 @@ # html_last_updated_fmt = '%b %d, %Y' html_last_updated_fmt = '%Y-%m-%d %H:%M' +# Add any paths that contain "extra" files, such as .htaccess or +# robots.txt. +html_extra_path = ['_extra'] + # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 5fbd575..e04d703 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -37,12 +37,14 @@ # keystone token credential [key_manager] + auth_url = 'http://192.169.5.254:5000' auth_type = 'keystone_token' token = '5b4de0bb77064f289f7cc58e33bea8c7' project_id = 'a1e19934af81420d980a5d02b4afe9fb' # keystone password credential [key_manager] + auth_url = 'http://192.169.5.254:5000' auth_type = 'keystone_password' username = 'admin' password = 'passw0rd1' @@ -64,8 +66,9 @@ from castellan.common import utils - CONF = - context = utils.credential_factory(conf=CONF, context=None) + CONF = cfg.CONF + CONF(default_config_files=['~/castellan.conf']) + context = utils.credential_factory(conf=CONF) Now you can go ahead and pass the context and use it for authentication. diff --git a/playbooks/devstack/post.yaml b/playbooks/devstack/post.yaml new file mode 100644 index 0000000..db7ca7d --- /dev/null +++ b/playbooks/devstack/post.yaml @@ -0,0 +1,4 @@ +- hosts: all + roles: + - fetch-tox-output + - fetch-stestr-output diff --git a/playbooks/devstack/pre.yaml b/playbooks/devstack/pre.yaml new file mode 100644 index 0000000..3ec41c9 --- /dev/null +++ b/playbooks/devstack/pre.yaml @@ -0,0 +1,8 @@ +- hosts: all + roles: + - run-devstack + - role: bindep + bindep_profile: test + bindep_dir: "{{ zuul_work_dir }}" + - test-setup + - ensure-tox diff --git a/playbooks/devstack/run.yaml b/playbooks/devstack/run.yaml new file mode 100644 index 0000000..22f8209 --- /dev/null +++ b/playbooks/devstack/run.yaml @@ -0,0 +1,3 @@ +- hosts: all + roles: + - tox diff --git a/releasenotes/notes/add-vault-provider-29a4c19fe67ab51f.yaml b/releasenotes/notes/add-vault-provider-29a4c19fe67ab51f.yaml new file mode 100644 index 0000000..4f646f9 --- /dev/null +++ b/releasenotes/notes/add-vault-provider-29a4c19fe67ab51f.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added a new provider for Vault (https://www.vaultproject.io/) \ No newline at end of file diff --git a/releasenotes/notes/deprecate-auth-endpoint-b91a3e67b5c7263f.yaml b/releasenotes/notes/deprecate-auth-endpoint-b91a3e67b5c7263f.yaml new file mode 100644 index 0000000..62ae236 --- /dev/null +++ b/releasenotes/notes/deprecate-auth-endpoint-b91a3e67b5c7263f.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - | + Config option barbican/auth_endpoint is unnecessary and deprecated in + favor of the more standard key_manager/auth_url. diff --git a/releasenotes/notes/support-legacy-fixed-key-id-9fa897b547111610.yaml b/releasenotes/notes/support-legacy-fixed-key-id-9fa897b547111610.yaml new file mode 100644 index 0000000..9ad956a --- /dev/null +++ b/releasenotes/notes/support-legacy-fixed-key-id-9fa897b547111610.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Enhance the key manager to handle requests containing the special (all + zeros) managed object ID associated with Cinder's and Nova's legacy + ConfKeyManager. The purpose of this feature is to help users migrate from + the ConfKeyManager to a modern key manager such as Barbican. The feature + works by ensuring the ConfKeyManager's all-zeros key ID continues to + function when Barbican or Vault is the key manager. diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 0000000..cbbe930 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +# 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. + +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'openstackdocstheme', + 'reno.sphinxext', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +repository_name = 'openstack/castellan' +bug_project = 'castellan' +bug_tag = 'doc' +project = u'Castellan Release Notes' +copyright = u'2017, Castellan Developers' + +# Release notes do not need a version number in the title, they +# cover multiple releases. +# The full version, including alpha/beta/rc tags. +release = '' +# The short X.Y version. +version = '' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'openstackdocs' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%Y-%m-%d %H:%M' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'CastellanReleaseNotesdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'CastellanReleaseNotes.tex', + u'Castellan Release Notes Documentation', + u'Castellan Developers', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'castellanreleasenotes', + u'Castellan Release Notes Documentation', + [u'Castellan Developers'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'CastellanReleaseNotes', + u'Castellan Release Notes Documentation', + u'Castellan Developers', 'CastellanReleaseNotes', + 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + +# -- Options for Internationalization output ------------------------------ +locale_dirs = ['locale/'] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 0000000..7237e42 --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,9 @@ +========================= + Castellan Release Notes +========================= + +.. toctree:: + :maxdepth: 1 + + unreleased + pike diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 0000000..e43bfc0 --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +=================================== + Pike Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/pike diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 0000000..cd22aab --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================== + Current Series Release Notes +============================== + +.. release-notes:: diff --git a/requirements.txt b/requirements.txt index dc856ae..15c81d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,12 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 Babel!=2.4.0,>=2.3.4 # BSD -cryptography>=1.6 # BSD/Apache-2.0 -python-barbicanclient>=4.0.0 # Apache-2.0 -oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0 -oslo.context>=2.14.0 # Apache-2.0 -oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 -oslo.log>=3.22.0 # Apache-2.0 -oslo.utils>=3.20.0 # Apache-2.0 -keystoneauth1>=2.21.0 # Apache-2.0 +cryptography!=2.0,>=1.9 # BSD/Apache-2.0 +python-barbicanclient!=4.5.0,!=4.5.1,>=4.0.0 # Apache-2.0 +oslo.config>=5.1.0 # Apache-2.0 +oslo.context>=2.19.2 # Apache-2.0 +oslo.i18n>=3.15.3 # Apache-2.0 +oslo.log>=3.30.0 # Apache-2.0 +oslo.utils>=3.33.0 # Apache-2.0 +stevedore>=1.20.0 # Apache-2.0 +keystoneauth1>=3.3.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index eba5c55..993a292 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,10 @@ castellan.tests.functional.config = castellan.tests.functional.config:list_opts castellan.config = castellan.options:list_opts +castellan.drivers = + barbican = castellan.key_manager.barbican_key_manager:BarbicanKeyManager + vault = castellan.key_manager.vault_key_manager:VaultKeyManager + [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/test-requirements.txt b/test-requirements.txt index 3637e7a..3dba5b5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,12 +4,14 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 -python-barbicanclient>=4.0.0 # Apache-2.0 -python-subunit>=0.0.18 # Apache-2.0/BSD +python-barbicanclient!=4.5.0,!=4.5.1,>=4.0.0 # Apache-2.0 +python-subunit>=1.0.0 # Apache-2.0/BSD sphinx>=1.6.2 # BSD -openstackdocstheme>=1.11.0 # Apache-2.0 +openstackdocstheme>=1.17.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD -testtools>=1.4.0 # MIT +testtools>=2.2.0 # MIT bandit>=1.1.0 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 +pifpaf>=0.10.0 # Apache-2.0 diff --git a/tools/setup-vault-env.sh b/tools/setup-vault-env.sh new file mode 100755 index 0000000..decde25 --- /dev/null +++ b/tools/setup-vault-env.sh @@ -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 + +$* diff --git a/tools/tox_install.sh b/tools/tox_install.sh deleted file mode 100755 index 5cc33e8..0000000 --- a/tools/tox_install.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -# [liujiong] This file is refer to tox_install.sh in neutron-lib. -# Library constraint file contains this library version pin that is in conflict -# with installing the library from source. We should replace the version pin in -# the constraints file before applying it for from-source installation. - -ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner -BRANCH_NAME=master -LIB_NAME=castellan -requirements_installed=$(echo "import openstack_requirements" | python 2>/dev/null ; echo $?) - -set -e - -CONSTRAINTS_FILE=$1 -shift - -install_cmd="pip install" -mydir=$(mktemp -dt "$LIB_NAME-tox_install-XXXXXXX") -trap "rm -rf $mydir" EXIT -localfile=$mydir/upper-constraints.txt -if [[ $CONSTRAINTS_FILE != http* ]]; then - CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE -fi -curl $CONSTRAINTS_FILE -k -o $localfile -install_cmd="$install_cmd -c$localfile" - -if [ $requirements_installed -eq 0 ]; then - echo "Requirements already installed; using existing package" -elif [ -x "$ZUUL_CLONER" ]; then - pushd $mydir - $ZUUL_CLONER --cache-dir \ - /opt/git \ - --branch $BRANCH_NAME \ - git://git.openstack.org \ - openstack/requirements - cd openstack/requirements - $install_cmd -e . - popd -else - if [ -z "$REQUIREMENTS_PIP_LOCATION" ]; then - REQUIREMENTS_PIP_LOCATION="git+https://git.openstack.org/openstack/requirements@$BRANCH_NAME#egg=requirements" - fi - $install_cmd -U -e ${REQUIREMENTS_PIP_LOCATION} -fi - -# This is the main purpose of the script: Allow local installation of -# the current repo. It is listed in constraints file and thus any -# install will be constrained and we need to unconstrain it. -edit-constraints $localfile -- $LIB_NAME "-e file://$PWD#egg=$LIB_NAME" - -$install_cmd -U $* -exit $? diff --git a/tox.ini b/tox.ini index 51d39f8..3debb18 100644 --- a/tox.ini +++ b/tox.ini @@ -5,11 +5,13 @@ [testenv] usedevelop = True -install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +install_command = pip install {opts} {packages} setenv = VIRTUAL_ENV={envdir} OS_TEST_PATH=./castellan/tests/unit -deps = -r{toxinidir}/requirements.txt +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' @@ -44,15 +46,26 @@ [testenv:docs] commands = python setup.py build_sphinx +[testenv:releasenotes] +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + [testenv:functional] 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 = 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 +commands = + {toxinidir}/tools/setup-vault-env.sh pifpaf -e VAULT_TEST run vault -- python setup.py testr --slowest --testr-args='{posargs}' [testenv:genconfig] commands =