From 27befcb85c9b042bcd09b116d73a160fd38329d3 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 8 Mar 2016 15:34:12 -0500 Subject: [PATCH] Adding KmipEngine support for Destroy This change adds support for the Destroy operation to the KmipEngine. New exceptions and test cases are included. --- kmip/core/exceptions.py | 36 +++ kmip/services/server/engine.py | 96 ++++++++ .../tests/unit/services/server/test_engine.py | 216 +++++++++++++++++- 3 files changed, 343 insertions(+), 5 deletions(-) diff --git a/kmip/core/exceptions.py b/kmip/core/exceptions.py index 0b4f42e..5420da9 100644 --- a/kmip/core/exceptions.py +++ b/kmip/core/exceptions.py @@ -57,6 +57,24 @@ class CryptographicFailure(KmipError): ) +class IndexOutOfBounds(KmipError): + """ + An error generated when exceeding the attribute instance limit. + """ + + def __init__(self, message): + """ + Create an IndexOutOfBounds exception. + + Args: + message (string): A string containing information about the error. + """ + super(IndexOutOfBounds, self).__init__( + reason=enums.ResultReason.INDEX_OUT_OF_BOUNDS, + message=message + ) + + class InvalidField(KmipError): """ An error generated when an invalid field value is processed. @@ -93,6 +111,24 @@ class InvalidMessage(KmipError): ) +class ItemNotFound(KmipError): + """ + An error generated when a request item cannot be located. + """ + + def __init__(self, message): + """ + Create an ItemNotFound exception. + + Args: + message (string): A string containing information about the error. + """ + super(ItemNotFound, self).__init__( + reason=enums.ResultReason.ITEM_NOT_FOUND, + message=message + ) + + class OperationNotSupported(KmipError): """ An error generated when an unsupported operation is invoked. diff --git a/kmip/services/server/engine.py b/kmip/services/server/engine.py index 9443a9e..a6465d2 100644 --- a/kmip/services/server/engine.py +++ b/kmip/services/server/engine.py @@ -14,22 +14,31 @@ # under the License. import logging +import sqlalchemy + +from sqlalchemy.orm import exc + import threading import time import kmip +from kmip.core import attributes from kmip.core import enums from kmip.core import exceptions from kmip.core.messages import contents from kmip.core.messages import messages +from kmip.core.messages.payloads import destroy from kmip.core.messages.payloads import discover_versions from kmip.core.messages.payloads import query from kmip.core import misc +from kmip.pie import sqltypes +from kmip.pie import objects + from kmip.services.server.crypto import engine @@ -47,6 +56,8 @@ class KmipEngine(object): * User authentication * Batch processing options: UNDO * Asynchronous operations + * Operation policies + * Object archival """ def __init__(self): @@ -56,6 +67,16 @@ class KmipEngine(object): self._logger = logging.getLogger(__name__) self._cryptography_engine = engine.CryptographyEngine() + + self._data_store = sqlalchemy.create_engine( + 'sqlite:///:memory:', + echo=False + ) + sqltypes.Base.metadata.create_all(self._data_store) + self._data_store_session_factory = sqlalchemy.orm.sessionmaker( + bind=self._data_store + ) + self._lock = threading.RLock() self._id_placeholder = None @@ -68,6 +89,17 @@ class KmipEngine(object): self._protocol_version = self._protocol_versions[0] + self._object_map = { + enums.ObjectType.CERTIFICATE: objects.X509Certificate, + enums.ObjectType.SYMMETRIC_KEY: objects.SymmetricKey, + enums.ObjectType.PUBLIC_KEY: objects.PublicKey, + enums.ObjectType.PRIVATE_KEY: objects.PrivateKey, + enums.ObjectType.SPLIT_KEY: None, + enums.ObjectType.TEMPLATE: None, + enums.ObjectType.SECRET_DATA: objects.SecretData, + enums.ObjectType.OPAQUE_DATA: objects.OpaqueObject + } + def _kmip_version_supported(supported): def decorator(function): def wrapper(self, *args, **kwargs): @@ -266,6 +298,9 @@ class KmipEngine(object): def _process_batch(self, request_batch, batch_handling, batch_order): response_batch = list() + + self._data_session = self._data_store_session_factory() + for batch_item in request_batch: error_occurred = False @@ -342,6 +377,8 @@ class KmipEngine(object): return response_batch def _process_operation(self, operation, payload): + if operation == enums.Operation.DESTROY: + return self._process_destroy(payload) if operation == enums.Operation.QUERY: return self._process_query(payload) elif operation == enums.Operation.DISCOVER_VERSIONS: @@ -353,6 +390,64 @@ class KmipEngine(object): ) ) + @_kmip_version_supported('1.0') + def _process_destroy(self, payload): + self._logger.info("Processing operation: Destroy") + + if payload.unique_identifier: + unique_identifier = payload.unique_identifier.value + else: + unique_identifier = self._id_placeholder + + try: + object_type = self._data_session.query( + objects.ManagedObject._object_type + ).filter( + objects.ManagedObject.unique_identifier == unique_identifier + ).one()[0] + except exc.NoResultFound as e: + self._logger.warning( + "Could not identify object type for object: {0}".format( + unique_identifier + ) + ) + self._logger.exception(e) + raise exceptions.ItemNotFound( + "Could not locate object: {0}".format(unique_identifier) + ) + except exc.MultipleResultsFound as e: + self._logger.warning( + "Multiple objects found for ID: {0}".format( + unique_identifier + ) + ) + raise e + + table = self._object_map.get(object_type) + if table is None: + name = object_type.name + raise exceptions.InvalidField( + "The {0} object type is not supported.".format( + ''.join( + [x.capitalize() for x in name[9:].split('_')] + ) + ) + ) + + # TODO (peterhamilton) Process attributes to see if destroy possible + # 1. Check object state. If invalid, error out. + # 2. Check object deactivation date. If invalid, error out. + + self._data_session.query(table).filter( + table.unique_identifier == unique_identifier + ).delete() + + response_payload = destroy.DestroyResponsePayload( + unique_identifier=attributes.UniqueIdentifier(unique_identifier) + ) + + return response_payload + @_kmip_version_supported('1.0') def _process_query(self, payload): self._logger.info("Processing operation: Query") @@ -368,6 +463,7 @@ class KmipEngine(object): if enums.QueryFunction.QUERY_OPERATIONS in queries: operations = list([ + contents.Operation(enums.Operation.DESTROY), contents.Operation(enums.Operation.QUERY) ]) diff --git a/kmip/tests/unit/services/server/test_engine.py b/kmip/tests/unit/services/server/test_engine.py index f13baed..d566106 100644 --- a/kmip/tests/unit/services/server/test_engine.py +++ b/kmip/tests/unit/services/server/test_engine.py @@ -14,11 +14,16 @@ # under the License. import mock +import sqlalchemy + +from sqlalchemy.orm import exc + import testtools import time import kmip +from kmip.core import attributes from kmip.core import enums from kmip.core import exceptions from kmip.core import misc @@ -27,9 +32,13 @@ from kmip.core import objects from kmip.core.messages import contents from kmip.core.messages import messages +from kmip.core.messages.payloads import destroy from kmip.core.messages.payloads import discover_versions from kmip.core.messages.payloads import query +from kmip.pie import objects as pie_objects +from kmip.pie import sqltypes + from kmip.services.server import engine @@ -50,6 +59,14 @@ class TestKmipEngine(testtools.TestCase): def setUp(self): super(TestKmipEngine, self).setUp() + self.engine = sqlalchemy.create_engine( + 'sqlite:///:memory:', + ) + sqltypes.Base.metadata.create_all(self.engine) + self.session_factory = sqlalchemy.orm.sessionmaker( + bind=self.engine + ) + def tearDown(self): super(TestKmipEngine, self).tearDown() @@ -621,12 +638,15 @@ class TestKmipEngine(testtools.TestCase): e = engine.KmipEngine() e._logger = mock.MagicMock() + e._process_destroy = mock.MagicMock() e._process_query = mock.MagicMock() e._process_discover_versions = mock.MagicMock() + e._process_operation(enums.Operation.DESTROY, None) e._process_operation(enums.Operation.QUERY, None) e._process_operation(enums.Operation.DISCOVER_VERSIONS, None) + e._process_destroy.assert_called_with(None) e._process_query.assert_called_with(None) e._process_discover_versions.assert_called_with(None) @@ -649,6 +669,178 @@ class TestKmipEngine(testtools.TestCase): *args ) + def test_destroy(self): + """ + Test that a Destroy request can be processed correctly. + """ + e = engine.KmipEngine() + e._data_store = self.engine + e._data_store_session_factory = self.session_factory + e._data_session = e._data_store_session_factory() + e._logger = mock.MagicMock() + + obj_a = pie_objects.OpaqueObject(b'', enums.OpaqueDataType.NONE) + obj_b = pie_objects.OpaqueObject(b'', enums.OpaqueDataType.NONE) + + e._data_session.add(obj_a) + e._data_session.add(obj_b) + e._data_session.commit() + e._data_session = e._data_store_session_factory() + + id_a = str(obj_a.unique_identifier) + id_b = str(obj_b.unique_identifier) + + # Test by specifying the ID of the object to destroy. + payload = destroy.DestroyRequestPayload( + unique_identifier=attributes.UniqueIdentifier(id_a) + ) + + response_payload = e._process_destroy(payload) + e._data_session.commit() + e._data_session = e._data_store_session_factory() + + e._logger.info.assert_called_once_with( + "Processing operation: Destroy" + ) + self.assertEqual(str(id_a), response_payload.unique_identifier.value) + self.assertRaises( + exc.NoResultFound, + e._data_session.query(pie_objects.OpaqueObject).filter( + pie_objects.ManagedObject.unique_identifier == id_a + ).one + ) + + e._data_session.commit() + e._data_store_session_factory() + e._logger.reset_mock() + e._id_placeholder = str(id_b) + + # Test by using the ID placeholder to specify the object to destroy. + payload = destroy.DestroyRequestPayload() + + response_payload = e._process_destroy(payload) + e._data_session.commit() + e._data_session = e._data_store_session_factory() + + e._logger.info.assert_called_once_with( + "Processing operation: Destroy" + ) + self.assertEqual(str(id_b), response_payload.unique_identifier.value) + self.assertRaises( + exc.NoResultFound, + e._data_session.query(pie_objects.OpaqueObject).filter( + pie_objects.ManagedObject.unique_identifier == id_b + ).one + ) + + e._data_session.commit() + + def test_destroy_missing_object(self): + """ + Test that an ItemNotFound error is generated when attempting to + destroy an object that does not exist. + """ + e = engine.KmipEngine() + e._data_store = self.engine + e._data_store_session_factory = self.session_factory + e._data_session = e._data_store_session_factory() + e._logger = mock.MagicMock() + + payload = destroy.DestroyRequestPayload( + unique_identifier=attributes.UniqueIdentifier('1') + ) + + args = (payload, ) + regex = "Could not locate object: 1" + self.assertRaisesRegexp( + exceptions.ItemNotFound, + regex, + e._process_destroy, + *args + ) + e._data_session.commit() + e._logger.info.assert_called_once_with( + "Processing operation: Destroy" + ) + e._logger.warning.assert_called_once_with( + "Could not identify object type for object: 1" + ) + self.assertTrue(e._logger.exception.called) + + def test_destroy_multiple_objects(self): + """ + Test that a sqlalchemy.orm.exc.MultipleResultsFound error is generated + when multiple objects map to the same object ID. + """ + e = engine.KmipEngine() + e._data_store = self.engine + e._data_store_session_factory = self.session_factory + e._data_session = e._data_store_session_factory() + test_exception = exc.MultipleResultsFound() + e._data_session.query = mock.MagicMock(side_effect=test_exception) + e._logger = mock.MagicMock() + + payload = destroy.DestroyRequestPayload( + unique_identifier=attributes.UniqueIdentifier('1') + ) + + args = (payload, ) + self.assertRaises( + exc.MultipleResultsFound, + e._process_destroy, + *args + ) + e._data_session.commit() + e._logger.info.assert_called_once_with( + "Processing operation: Destroy" + ) + e._logger.warning.assert_called_once_with( + "Multiple objects found for ID: 1" + ) + + def test_destroy_unsupported_object_type(self): + """ + Test that an InvalidField error is generated when attempting to + destroy an unsupported object type. + """ + e = engine.KmipEngine() + e._object_map = {enums.ObjectType.OPAQUE_DATA: None} + e._data_store = self.engine + e._data_store_session_factory = self.session_factory + e._data_session = e._data_store_session_factory() + e._logger = mock.MagicMock() + + obj_a = pie_objects.OpaqueObject(b'', enums.OpaqueDataType.NONE) + + e._data_session.add(obj_a) + e._data_session.commit() + e._data_session = e._data_store_session_factory() + + id_a = str(obj_a.unique_identifier) + + payload = destroy.DestroyRequestPayload( + unique_identifier=attributes.UniqueIdentifier(id_a) + ) + + args = (payload, ) + name = enums.ObjectType.OPAQUE_DATA.name + regex = "The {0} object type is not supported.".format( + ''.join( + [x.capitalize() for x in name[9:].split('_')] + ) + ) + + self.assertRaisesRegexp( + exceptions.InvalidField, + regex, + e._process_destroy, + *args + ) + e._data_session.commit() + e._logger.info.assert_called_once_with( + "Processing operation: Destroy" + ) + def test_query(self): """ Test that a Query request can be processed correctly, for different @@ -678,8 +870,15 @@ class TestKmipEngine(testtools.TestCase): e._logger.info.assert_called_once_with("Processing operation: Query") self.assertIsInstance(result, query.QueryResponsePayload) self.assertIsNotNone(result.operations) - self.assertEqual(1, len(result.operations)) - self.assertEqual(enums.Operation.QUERY, result.operations[0].value) + self.assertEqual(2, len(result.operations)) + self.assertEqual( + enums.Operation.DESTROY, + result.operations[0].value + ) + self.assertEqual( + enums.Operation.QUERY, + result.operations[1].value + ) self.assertEqual(list(), result.object_types) self.assertIsNotNone(result.vendor_identification) self.assertEqual( @@ -698,11 +897,18 @@ class TestKmipEngine(testtools.TestCase): e._logger.info.assert_called_once_with("Processing operation: Query") self.assertIsNotNone(result.operations) - self.assertEqual(2, len(result.operations)) - self.assertEqual(enums.Operation.QUERY, result.operations[0].value) + self.assertEqual(3, len(result.operations)) + self.assertEqual( + enums.Operation.DESTROY, + result.operations[0].value + ) + self.assertEqual( + enums.Operation.QUERY, + result.operations[1].value + ) self.assertEqual( enums.Operation.DISCOVER_VERSIONS, - result.operations[1].value + result.operations[2].value ) def test_discover_versions(self):