diff --git a/nova/context.py b/nova/context.py index e78636cdde..18fc2e195e 100644 --- a/nova/context.py +++ b/nova/context.py @@ -109,7 +109,7 @@ class RequestContext(object): if service_catalog: # Only include required parts of service_catalog self.service_catalog = [s for s in service_catalog - if s.get('type') in ('volume', 'volumev2')] + if s.get('type') in ('volume', 'volumev2', 'key-manager')] else: # if list is empty or none self.service_catalog = [] diff --git a/nova/keymgr/barbican.py b/nova/keymgr/barbican.py new file mode 100644 index 0000000000..d9de15950f --- /dev/null +++ b/nova/keymgr/barbican.py @@ -0,0 +1,346 @@ +# Copyright (c) 2015 The Johns Hopkins University/Applied Physics Laboratory +# 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. + +""" +Key manager implementation for Barbican +""" + +import array +import base64 +import binascii + +from barbicanclient import client as barbican_client +from keystoneclient import session +from oslo.config import cfg +from oslo.utils import excutils + +from nova import exception +from nova.i18n import _ +from nova.i18n import _LE +from nova.keymgr import key as keymgr_key +from nova.keymgr import key_mgr +from nova.openstack.common import log as logging + +barbican_opts = [ + cfg.StrOpt('catalog_info', + default='key-manager:barbican:public', + help='Info to match when looking for barbican in the service ' + 'catalog. Format is: separated values of the form: ' + '::'), + cfg.StrOpt('endpoint_template', + help='Override service catalog lookup with template for ' + 'barbican endpoint e.g. ' + 'http://localhost:9311/v1/%(project_id)s'), + cfg.StrOpt('os_region_name', + help='Region name of this node'), +] + +CONF = cfg.CONF +BARBICAN_OPT_GROUP = 'barbican' + +CONF.register_opts(barbican_opts, group=BARBICAN_OPT_GROUP) + +session.Session.register_conf_options(CONF, BARBICAN_OPT_GROUP) + +LOG = logging.getLogger(__name__) + + +class BarbicanKeyManager(key_mgr.KeyManager): + """Key Manager Interface that wraps the Barbican client API.""" + + def __init__(self): + self._barbican_client = None + self._base_url = None + + def _get_barbican_client(self, ctxt): + """Creates a client to connect to the Barbican service. + + :param ctxt: the user context for authentication + :return: a Barbican Client object + :raises Forbidden: if the ctxt is None + """ + + if not self._barbican_client: + # Confirm context is provided, if not raise forbidden + if not ctxt: + msg = _("User is not authorized to use key manager.") + LOG.error(msg) + raise exception.Forbidden(msg) + + try: + _SESSION = session.Session.load_from_conf_options( + CONF, + BARBICAN_OPT_GROUP) + + auth = ctxt.get_auth_plugin() + service_type, service_name, interface = (CONF. + barbican. + catalog_info. + split(':')) + region_name = CONF.barbican.os_region_name + service_parameters = {'service_type': service_type, + 'service_name': service_name, + 'interface': interface, + 'region_name': region_name} + + if CONF.barbican.endpoint_template: + self._base_url = (CONF.barbican.endpoint_template % + ctxt.to_dict()) + else: + self._base_url = _SESSION.get_endpoint( + auth, **service_parameters) + + # the barbican endpoint can't have the '/v1' on the end + self._barbican_endpoint = self._base_url.rpartition('/')[0] + + sess = session.Session(auth=auth) + self._barbican_client = barbican_client.Client( + session=sess, + endpoint=self._barbican_endpoint) + + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error creating Barbican client: %s"), e) + + return self._barbican_client + + def create_key(self, ctxt, expiration=None, name='Nova Compute Key', + payload_content_type='application/octet-stream', mode='CBC', + algorithm='AES', length=256): + """Creates a key. + + :param ctxt: contains information of the user and the environment + for the request (nova/context.py) + :param expiration: the date the key will expire + :param name: a friendly name for the secret + :param payload_content_type: the format/type of the secret data + :param mode: the algorithm mode (e.g. CBC or CTR mode) + :param algorithm: the algorithm associated with the secret + :param length: the bit length of the secret + + :return: the UUID of the new key + :raises Exception: if key creation fails + """ + barbican_client = self._get_barbican_client(ctxt) + + try: + key_order = barbican_client.orders.create_key( + name, + algorithm, + length, + mode, + payload_content_type, + expiration) + order_ref = key_order.submit() + order = barbican_client.orders.get(order_ref) + return self._retrieve_secret_uuid(order.secret_ref) + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error creating key: %s"), e) + + def store_key(self, ctxt, key, expiration=None, name='Nova Compute Key', + payload_content_type='application/octet-stream', + payload_content_encoding='base64', algorithm='AES', + bit_length=256, mode='CBC', from_copy=False): + """Stores (i.e., registers) a key with the key manager. + + :param ctxt: contains information of the user and the environment for + the request (nova/context.py) + :param key: the unencrypted secret data. Known as "payload" to the + barbicanclient api + :param expiration: the expiration time of the secret in ISO 8601 + format + :param name: a friendly name for the key + :param payload_content_type: the format/type of the secret data + :param payload_content_encoding: the encoding of the secret data + :param algorithm: the algorithm associated with this secret key + :param bit_length: the bit length of this secret key + :param mode: the algorithm mode used with this secret key + :param from_copy: establishes whether the function is being used + to copy a key. In case of the latter, it does not + try to decode the key + + :returns: the UUID of the stored key + :raises Exception: if key storage fails + """ + barbican_client = self._get_barbican_client(ctxt) + + try: + if key.get_algorithm(): + algorithm = key.get_algorithm() + if payload_content_type == 'text/plain': + payload_content_encoding = None + encoded_key = key.get_encoded() + elif (payload_content_type == 'application/octet-stream' and + not from_copy): + key_list = key.get_encoded() + string_key = ''.join(map(lambda byte: "%02x" % byte, key_list)) + encoded_key = base64.b64encode(binascii.unhexlify(string_key)) + else: + encoded_key = key.get_encoded() + secret = barbican_client.secrets.create(name, + encoded_key, + payload_content_type, + payload_content_encoding, + algorithm, + bit_length, + mode, + expiration) + secret_ref = secret.store() + return self._retrieve_secret_uuid(secret_ref) + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error storing key: %s"), e) + + def copy_key(self, ctxt, key_id): + """Copies (i.e., clones) a key stored by barbican. + + :param ctxt: contains information of the user and the environment for + the request (nova/context.py) + :param key_id: the UUID of the key to copy + :return: the UUID of the key copy + :raises Exception: if key copying fails + """ + + try: + secret = self._get_secret(ctxt, key_id) + con_type = secret.content_types['default'] + secret_data = self._get_secret_data(secret, + payload_content_type=con_type) + key = keymgr_key.SymmetricKey(secret.algorithm, secret_data) + copy_uuid = self.store_key(ctxt, key, secret.expiration, + secret.name, con_type, + 'base64', + secret.algorithm, secret.bit_length, + secret.mode, True) + return copy_uuid + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error copying key: %s"), e) + + def _create_secret_ref(self, key_id): + """Creates the URL required for accessing a secret. + + :param key_id: the UUID of the key to copy + + :return: the URL of the requested secret + """ + if not key_id: + msg = "Key ID is None" + raise exception.KeyManagerError(msg) + return self._base_url + "/secrets/" + key_id + + def _retrieve_secret_uuid(self, secret_ref): + """Retrieves the UUID of the secret from the secret_ref. + + :param secret_ref: the href of the secret + + :return: the UUID of the secret + """ + + # The secret_ref is assumed to be of a form similar to + # http://host:9311/v1/secrets/d152fa13-2b41-42ca-a934-6c21566c0f40 + # with the UUID at the end. This command retrieves everything + # after the last '/', which is the UUID. + return secret_ref.rpartition('/')[2] + + def _get_secret_data(self, + secret, + payload_content_type='application/octet-stream'): + """Retrieves the secret data given a secret and content_type. + + :param ctxt: contains information of the user and the environment for + the request (nova/context.py) + :param secret: the secret from barbican with the payload of data + :param payload_content_type: the format/type of the secret data + + :returns: the secret data + :raises Exception: if data cannot be retrieved + """ + try: + generated_data = secret.payload + if payload_content_type == 'application/octet-stream': + secret_data = base64.b64encode(generated_data) + else: + secret_data = generated_data + return secret_data + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error getting secret data: %s"), e) + + def _get_secret(self, ctxt, key_id): + """Returns the metadata of the secret. + + :param ctxt: contains information of the user and the environment for + the request (nova/context.py) + :param key_id: UUID of the secret + + :return: the secret's metadata + :raises Exception: if there is an error retrieving the data + """ + + barbican_client = self._get_barbican_client(ctxt) + + try: + secret_ref = self._create_secret_ref(key_id) + return barbican_client.secrets.get(secret_ref) + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error getting secret metadata: %s"), e) + + def get_key(self, ctxt, key_id, + payload_content_type='application/octet-stream'): + """Retrieves the specified key. + + :param ctxt: contains information of the user and the environment for + the request (nova/context.py) + :param key_id: the UUID of the key to retrieve + :param payload_content_type: The format/type of the secret data + + :return: SymmetricKey representation of the key + :raises Exception: if key retrieval fails + """ + try: + secret = self._get_secret(ctxt, key_id) + secret_data = self._get_secret_data(secret, + payload_content_type) + if payload_content_type == 'application/octet-stream': + # convert decoded string to list of unsigned ints for each byte + key_data = array.array('B', + base64.b64decode(secret_data)).tolist() + else: + key_data = secret_data + key = keymgr_key.SymmetricKey(secret.algorithm, key_data) + return key + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error getting key: %s"), e) + + def delete_key(self, ctxt, key_id): + """Deletes the specified key. + + :param ctxt: contains information of the user and the environment for + the request (nova/context.py) + :param key_id: the UUID of the key to delete + :raises Exception: if key deletion fails + """ + barbican_client = self._get_barbican_client(ctxt) + + try: + secret_ref = self._create_secret_ref(key_id) + barbican_client.secrets.delete(secret_ref) + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Error deleting key: %s"), e) diff --git a/nova/tests/unit/keymgr/test_barbican.py b/nova/tests/unit/keymgr/test_barbican.py new file mode 100644 index 0000000000..36901f3660 --- /dev/null +++ b/nova/tests/unit/keymgr/test_barbican.py @@ -0,0 +1,223 @@ +# Copyright (c) 2015 The Johns Hopkins University/Applied Physics Laboratory +# 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 barbican key manager. +""" + +import array +import binascii + +import mock + +from nova import exception +from nova.keymgr import barbican +from nova.keymgr import key as keymgr_key +from nova.tests.unit.keymgr import test_key_mgr + + +class BarbicanKeyManagerTestCase(test_key_mgr.KeyManagerTestCase): + + def _create_key_manager(self): + return barbican.BarbicanKeyManager() + + def setUp(self): + super(BarbicanKeyManagerTestCase, self).setUp() + + # Create fake auth_token + self.ctxt = mock.Mock() + self.ctxt.auth_token = "fake_token" + + # Create mock barbican client + self._build_mock_barbican() + + # Create a key_id, secret_ref, pre_hex, and hex to use + self.key_id = "d152fa13-2b41-42ca-a934-6c21566c0f40" + self.secret_ref = ("http://host:9311/v1/secrets/" + self.key_id) + self.pre_hex = "AIDxQp2++uAbKaTVDMXFYIu8PIugJGqkK0JLqkU0rhY=" + self.hex = ("0080f1429dbefae01b29a4d50cc5c5608bbc3c8ba0246aa42b424baa4" + "534ae16") + self.key_mgr._base_url = "http://host:9311/v1" + self.addCleanup(self._restore) + + def _restore(self): + if hasattr(self, 'original_key'): + keymgr_key.SymmetricKey = self.original_key + + def _build_mock_barbican(self): + self.mock_barbican = mock.MagicMock(name='mock_barbican') + + # Set commonly used methods + self.get = self.mock_barbican.secrets.get + self.delete = self.mock_barbican.secrets.delete + self.store = self.mock_barbican.secrets.store + self.create = self.mock_barbican.secrets.create + + self.key_mgr._barbican_client = self.mock_barbican + + def _build_mock_symKey(self): + self.mock_symKey = mock.Mock() + + def fake_sym_key(alg, key): + self.mock_symKey.get_encoded.return_value = key + self.mock_symKey.get_algorithm.return_value = alg + return self.mock_symKey + self.original_key = keymgr_key.SymmetricKey + keymgr_key.SymmetricKey = fake_sym_key + + def test_copy_key(self): + # Create metadata for original secret + original_secret_metadata = mock.Mock() + original_secret_metadata.algorithm = mock.sentinel.alg + original_secret_metadata.bit_length = mock.sentinel.bit + original_secret_metadata.name = mock.sentinel.name + original_secret_metadata.expiration = mock.sentinel.expiration + original_secret_metadata.mode = mock.sentinel.mode + content_types = {'default': 'fake_type'} + original_secret_metadata.content_types = content_types + original_secret_data = mock.Mock() + original_secret_metadata.payload = original_secret_data + + # Create href for copied secret + copied_secret = mock.Mock() + copied_secret.store.return_value = 'http://test/uuid' + + # Set get and create return values + self.get.return_value = original_secret_metadata + self.create.return_value = copied_secret + + # Create the mock key + self._build_mock_symKey() + + # Copy the original + self.key_mgr.copy_key(self.ctxt, self.key_id) + + # Assert proper methods were called + self.get.assert_called_once_with(self.secret_ref) + self.create.assert_called_once_with( + mock.sentinel.name, + self.mock_symKey.get_encoded(), + content_types['default'], + 'base64', + mock.sentinel.alg, + mock.sentinel.bit, + mock.sentinel.mode, + mock.sentinel.expiration) + copied_secret.store.assert_called_once_with() + + def test_copy_null_context(self): + self.key_mgr._barbican_client = None + self.assertRaises(exception.Forbidden, + self.key_mgr.copy_key, None, self.key_id) + + def test_create_key(self): + # Create order_ref_url and assign return value + order_ref_url = ("http://localhost:9311/v1/None/orders/" + "4fe939b7-72bc-49aa-bd1e-e979589858af") + key_order = mock.Mock() + self.mock_barbican.orders.create_key.return_value = key_order + key_order.submit.return_value = order_ref_url + + # Create order and assign return value + order = mock.Mock() + order.secret_ref = self.secret_ref + self.mock_barbican.orders.get.return_value = order + + # Create the key, get the UUID + returned_uuid = self.key_mgr.create_key(self.ctxt) + + self.mock_barbican.orders.get.assert_called_once_with(order_ref_url) + self.assertEqual(returned_uuid, self.key_id) + + def test_create_null_context(self): + self.key_mgr._barbican_client = None + self.assertRaises(exception.Forbidden, + self.key_mgr.create_key, None) + + def test_delete_null_context(self): + self.key_mgr._barbican_client = None + self.assertRaises(exception.Forbidden, + self.key_mgr.delete_key, None, self.key_id) + + def test_delete_key(self): + self.key_mgr.delete_key(self.ctxt, self.key_id) + self.delete.assert_called_once_with(self.secret_ref) + + def test_delete_unknown_key(self): + self.assertRaises(exception.KeyManagerError, + self.key_mgr.delete_key, self.ctxt, None) + + @mock.patch('base64.b64encode') + def test_get_key(self, b64_mock): + b64_mock.return_value = self.pre_hex + content_type = 'application/octet-stream' + + key = self.key_mgr.get_key(self.ctxt, self.key_id, content_type) + + self.get.assert_called_once_with(self.secret_ref) + encoded = array.array('B', binascii.unhexlify(self.hex)).tolist() + self.assertEqual(key.get_encoded(), encoded) + + def test_get_null_context(self): + self.key_mgr._barbican_client = None + self.assertRaises(exception.Forbidden, + self.key_mgr.get_key, None, self.key_id) + + def test_get_unknown_key(self): + self.assertRaises(exception.KeyManagerError, + self.key_mgr.get_key, self.ctxt, None) + + def test_store_key_base64(self): + # Create Key to store + secret_key = array.array('B', [0x01, 0x02, 0xA0, 0xB3]).tolist() + _key = keymgr_key.SymmetricKey('AES', secret_key) + + # Define the return values + secret = mock.Mock() + self.create.return_value = secret + secret.store.return_value = self.secret_ref + + # Store the Key + returned_uuid = self.key_mgr.store_key(self.ctxt, _key, bit_length=32) + + self.create.assert_called_once_with('Nova Compute Key', + 'AQKgsw==', + 'application/octet-stream', + 'base64', + 'AES', 32, 'CBC', + None) + self.assertEqual(returned_uuid, self.key_id) + + def test_store_key_plaintext(self): + # Create the plaintext key + secret_key_text = "This is a test text key." + _key = keymgr_key.SymmetricKey('AES', secret_key_text) + + # Store the Key + self.key_mgr.store_key(self.ctxt, _key, + payload_content_type='text/plain', + payload_content_encoding=None) + self.create.assert_called_once_with('Nova Compute Key', + secret_key_text, + 'text/plain', + None, + 'AES', 256, 'CBC', + None) + self.assertEqual(self.store.call_count, 0) + + def test_store_null_context(self): + self.key_mgr._barbican_client = None + self.assertRaises(exception.Forbidden, + self.key_mgr.store_key, None, None) diff --git a/test-requirements.txt b/test-requirements.txt index 49c56cc348..8b044955aa 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,6 +10,7 @@ mock>=1.0 mox3>=0.7.0 MySQL-python psycopg2 +python-barbicanclient>=3.0.1 python-ironicclient>=0.2.1 python-subunit>=0.0.18 requests-mock>=0.5.1 # Apache-2.0