Adding KmipEngine support for Destroy

This change adds support for the Destroy operation to the KmipEngine.
New exceptions and test cases are included.
This commit is contained in:
Peter 2016-03-08 15:34:12 -05:00
parent 81222e23f1
commit 27befcb85c
3 changed files with 343 additions and 5 deletions

View File

@ -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): class InvalidField(KmipError):
""" """
An error generated when an invalid field value is processed. 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): class OperationNotSupported(KmipError):
""" """
An error generated when an unsupported operation is invoked. An error generated when an unsupported operation is invoked.

View File

@ -14,22 +14,31 @@
# under the License. # under the License.
import logging import logging
import sqlalchemy
from sqlalchemy.orm import exc
import threading import threading
import time import time
import kmip import kmip
from kmip.core import attributes
from kmip.core import enums from kmip.core import enums
from kmip.core import exceptions from kmip.core import exceptions
from kmip.core.messages import contents from kmip.core.messages import contents
from kmip.core.messages import messages 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 discover_versions
from kmip.core.messages.payloads import query from kmip.core.messages.payloads import query
from kmip.core import misc from kmip.core import misc
from kmip.pie import sqltypes
from kmip.pie import objects
from kmip.services.server.crypto import engine from kmip.services.server.crypto import engine
@ -47,6 +56,8 @@ class KmipEngine(object):
* User authentication * User authentication
* Batch processing options: UNDO * Batch processing options: UNDO
* Asynchronous operations * Asynchronous operations
* Operation policies
* Object archival
""" """
def __init__(self): def __init__(self):
@ -56,6 +67,16 @@ class KmipEngine(object):
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
self._cryptography_engine = engine.CryptographyEngine() 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._lock = threading.RLock()
self._id_placeholder = None self._id_placeholder = None
@ -68,6 +89,17 @@ class KmipEngine(object):
self._protocol_version = self._protocol_versions[0] 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 _kmip_version_supported(supported):
def decorator(function): def decorator(function):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
@ -266,6 +298,9 @@ class KmipEngine(object):
def _process_batch(self, request_batch, batch_handling, batch_order): def _process_batch(self, request_batch, batch_handling, batch_order):
response_batch = list() response_batch = list()
self._data_session = self._data_store_session_factory()
for batch_item in request_batch: for batch_item in request_batch:
error_occurred = False error_occurred = False
@ -342,6 +377,8 @@ class KmipEngine(object):
return response_batch return response_batch
def _process_operation(self, operation, payload): def _process_operation(self, operation, payload):
if operation == enums.Operation.DESTROY:
return self._process_destroy(payload)
if operation == enums.Operation.QUERY: if operation == enums.Operation.QUERY:
return self._process_query(payload) return self._process_query(payload)
elif operation == enums.Operation.DISCOVER_VERSIONS: 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') @_kmip_version_supported('1.0')
def _process_query(self, payload): def _process_query(self, payload):
self._logger.info("Processing operation: Query") self._logger.info("Processing operation: Query")
@ -368,6 +463,7 @@ class KmipEngine(object):
if enums.QueryFunction.QUERY_OPERATIONS in queries: if enums.QueryFunction.QUERY_OPERATIONS in queries:
operations = list([ operations = list([
contents.Operation(enums.Operation.DESTROY),
contents.Operation(enums.Operation.QUERY) contents.Operation(enums.Operation.QUERY)
]) ])

View File

@ -14,11 +14,16 @@
# under the License. # under the License.
import mock import mock
import sqlalchemy
from sqlalchemy.orm import exc
import testtools import testtools
import time import time
import kmip import kmip
from kmip.core import attributes
from kmip.core import enums from kmip.core import enums
from kmip.core import exceptions from kmip.core import exceptions
from kmip.core import misc 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 contents
from kmip.core.messages import messages 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 discover_versions
from kmip.core.messages.payloads import query 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 from kmip.services.server import engine
@ -50,6 +59,14 @@ class TestKmipEngine(testtools.TestCase):
def setUp(self): def setUp(self):
super(TestKmipEngine, self).setUp() 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): def tearDown(self):
super(TestKmipEngine, self).tearDown() super(TestKmipEngine, self).tearDown()
@ -621,12 +638,15 @@ class TestKmipEngine(testtools.TestCase):
e = engine.KmipEngine() e = engine.KmipEngine()
e._logger = mock.MagicMock() e._logger = mock.MagicMock()
e._process_destroy = mock.MagicMock()
e._process_query = mock.MagicMock() e._process_query = mock.MagicMock()
e._process_discover_versions = 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.QUERY, None)
e._process_operation(enums.Operation.DISCOVER_VERSIONS, 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_query.assert_called_with(None)
e._process_discover_versions.assert_called_with(None) e._process_discover_versions.assert_called_with(None)
@ -649,6 +669,178 @@ class TestKmipEngine(testtools.TestCase):
*args *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): def test_query(self):
""" """
Test that a Query request can be processed correctly, for different 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") e._logger.info.assert_called_once_with("Processing operation: Query")
self.assertIsInstance(result, query.QueryResponsePayload) self.assertIsInstance(result, query.QueryResponsePayload)
self.assertIsNotNone(result.operations) self.assertIsNotNone(result.operations)
self.assertEqual(1, len(result.operations)) self.assertEqual(2, len(result.operations))
self.assertEqual(enums.Operation.QUERY, result.operations[0].value) 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.assertEqual(list(), result.object_types)
self.assertIsNotNone(result.vendor_identification) self.assertIsNotNone(result.vendor_identification)
self.assertEqual( self.assertEqual(
@ -698,11 +897,18 @@ class TestKmipEngine(testtools.TestCase):
e._logger.info.assert_called_once_with("Processing operation: Query") e._logger.info.assert_called_once_with("Processing operation: Query")
self.assertIsNotNone(result.operations) self.assertIsNotNone(result.operations)
self.assertEqual(2, len(result.operations)) self.assertEqual(3, len(result.operations))
self.assertEqual(enums.Operation.QUERY, result.operations[0].value) self.assertEqual(
enums.Operation.DESTROY,
result.operations[0].value
)
self.assertEqual(
enums.Operation.QUERY,
result.operations[1].value
)
self.assertEqual( self.assertEqual(
enums.Operation.DISCOVER_VERSIONS, enums.Operation.DISCOVER_VERSIONS,
result.operations[1].value result.operations[2].value
) )
def test_discover_versions(self): def test_discover_versions(self):