diff --git a/castellan/key_manager/barbican_key_manager.py b/castellan/key_manager/barbican_key_manager.py index d73fc32..d7b9e26 100644 --- a/castellan/key_manager/barbican_key_manager.py +++ b/castellan/key_manager/barbican_key_manager.py @@ -572,3 +572,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.warn(_("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..6b5cee0 100644 --- a/castellan/key_manager/key_manager.py +++ b/castellan/key_manager/key_manager.py @@ -109,3 +109,18 @@ considered "non-existent" and completely invisible. """ pass + + @abc.abstractmethod + 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. + """ + pass 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/tests/functional/key_manager/test_key_manager.py b/castellan/tests/functional/key_manager/test_key_manager.py index 85ff73e..f62dee3 100644 --- a/castellan/tests/functional/key_manager/test_key_manager.py +++ b/castellan/tests/functional/key_manager/test_key_manager.py @@ -171,3 +171,66 @@ retrieved_object = self.key_mgr.get(self.ctxt, uuid) self.assertEqual(managed_object.get_encoded(), retrieved_object.get_encoded()) + + @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 obj in retrieved_objects: + self.assertFalse(obj.is_metadata_only()) + + @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 obj in retrieved_objects: + self.assertTrue(obj.is_metadata_only()) + + @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.assertTrue(object_1 in retrieved_objects) diff --git a/castellan/tests/unit/key_manager/mock_key_manager.py b/castellan/tests/unit/key_manager/mock_key_manager.py index 384a4b6..b981292 100644 --- a/castellan/tests/unit/key_manager/mock_key_manager.py +++ b/castellan/tests/unit/key_manager/mock_key_manager.py @@ -226,3 +226,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..44ca09d 100644 --- a/castellan/tests/unit/key_manager/test_barbican_key_manager.py +++ b/castellan/tests/unit/key_manager/test_barbican_key_manager.py @@ -72,6 +72,7 @@ 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 @@ -348,3 +349,60 @@ 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' + + 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_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_mock_key_manager.py b/castellan/tests/unit/key_manager/test_mock_key_manager.py index 4b0a40b..dd2d6e7 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) @@ -201,3 +206,30 @@ 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) + + 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) 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)