diff --git a/kmip/core/exceptions.py b/kmip/core/exceptions.py index b16623a..6636a38 100644 --- a/kmip/core/exceptions.py +++ b/kmip/core/exceptions.py @@ -228,6 +228,23 @@ class KeyFormatTypeNotSupported(KmipError): ) +class OperationFailure(KmipError): + """ + An exception raised upon the failure of a KMIP appliance operation. + """ + def __init__(self, status, reason, message): + """ + Construct the error message and attributes for the KMIP operation + failure. + + Args: + status: a ResultStatus enumeration + reason: a ResultReason enumeration + message: a string providing additional error information + """ + super(OperationFailure, self).__init__(status, reason, message) + + class OperationNotSupported(KmipError): """ An error generated when an unsupported operation is invoked. diff --git a/kmip/pie/client.py b/kmip/pie/client.py index 67ae471..1bc5e50 100644 --- a/kmip/pie/client.py +++ b/kmip/pie/client.py @@ -25,6 +25,8 @@ from kmip.core.factories import attributes from kmip.core.attributes import CryptographicParameters from kmip.core.attributes import DerivationParameters +from kmip.core.messages import payloads + from kmip.pie import exceptions from kmip.pie import factory from kmip.pie import objects as pobjects @@ -386,6 +388,49 @@ class ProxyKmipClient(object): message = result.result_message.value raise exceptions.KmipOperationFailure(status, reason, message) + @is_connected + def delete_attribute(self, unique_identifier=None, **kwargs): + """ + Delete an attribute from a KMIP managed object. + + Args: + unique_identifier (string): The ID of the managed object. + **kwargs (various): A placeholder for attribute values used to + identify the attribute to delete. For KMIP 1.0 - 1.4, the + supported parameters are: + attribute_name (string): The name of the attribute to + delete. Required. + attribute_index (int): The index of the attribute to + delete. Defaults to zero. + For KMIP 2.0+, the supported parameters are: + current_attribute (struct): A CurrentAttribute object + containing the attribute to delete. Required if the + attribute reference is not specified. + attribute_reference (struct): An AttributeReference + object containing the name of the attribute to + delete. Required if the current attribute is not + specified. + + Returns: + string: The ID of the managed object the attribute was deleted + from. + struct: A Primitive object representing the deleted attribute. + Only returned if used for KMIP 1.0 - 1.4 messages. + """ + request_payload = payloads.DeleteAttributeRequestPayload( + unique_identifier=unique_identifier, + attribute_name=kwargs.get("attribute_name"), + attribute_index=kwargs.get("attribute_index"), + current_attribute=kwargs.get("current_attribute"), + attribute_reference=kwargs.get("attribute_reference") + ) + response_payload = self.proxy.send_request_payload( + enums.Operation.DELETE_ATTRIBUTE, + request_payload + ) + + return response_payload.unique_identifier, response_payload.attribute + @is_connected def register(self, managed_object): """ diff --git a/kmip/services/kmip_client.py b/kmip/services/kmip_client.py index 38bbc44..9842063 100644 --- a/kmip/services/kmip_client.py +++ b/kmip/services/kmip_client.py @@ -39,6 +39,8 @@ from kmip.core.enums import ConformanceClause from kmip.core.enums import CredentialType from kmip.core.enums import Operation as OperationEnum +from kmip.core import exceptions + from kmip.core.factories.credentials import CredentialFactory from kmip.core import objects @@ -309,6 +311,86 @@ class KMIPProxy(object): pass self.socket = None + def send_request_payload(self, operation, payload, credential=None): + """ + Send a KMIP request. + + Args: + operation (enum): An Operation enumeration specifying the type + of operation to be requested. Required. + payload (struct): A RequestPayload structure containing the + parameters for a specific KMIP operation. Required. + credential (struct): A Credential structure containing + authentication information for the server. Optional, defaults + to None. + + Returns: + response (struct): A ResponsePayload structure containing the + results of the KMIP operation specified in the request. + + Raises: + TypeError: if the payload is not a RequestPayload instance or if + the operation and payload type do not match + InvalidMessage: if the response message does not have the right + number of response payloads, or does not match the request + operation + """ + if not isinstance(payload, payloads.RequestPayload): + raise TypeError( + "The request payload must be a RequestPayload object." + ) + + # TODO (peterhamilton) For now limit this to the new DeleteAttribute + # operation. Migrate over existing operations to use this method + # instead. + if operation == enums.Operation.DELETE_ATTRIBUTE: + if not isinstance(payload, payloads.DeleteAttributeRequestPayload): + raise TypeError( + "The request payload for the DeleteAttribute operation " + "must be a DeleteAttributeRequestPayload object." + ) + + batch_item = messages.RequestBatchItem( + operation=operation, + request_payload=payload + ) + + request_message = self._build_request_message(credential, [batch_item]) + response_message = self._send_and_receive_message(request_message) + + if len(response_message.batch_items) != 1: + raise exceptions.InvalidMessage( + "The response message does not have the right number of " + "requested operation results." + ) + + batch_item = response_message.batch_items[0] + + if batch_item.result_status.value != enums.ResultStatus.SUCCESS: + raise exceptions.OperationFailure( + batch_item.result_status.value, + batch_item.result_reason.value, + batch_item.result_message.value + ) + + if batch_item.operation.value != operation: + raise exceptions.InvalidMessage( + "The response message does not match the request operation." + ) + + # TODO (peterhamilton) Same as above for now. + if batch_item.operation.value == enums.Operation.DELETE_ATTRIBUTE: + if not isinstance( + batch_item.response_payload, + payloads.DeleteAttributeResponsePayload + ): + raise exceptions.InvalidMessage( + "Invalid response payload received for the " + "DeleteAttribute operation." + ) + + return batch_item.response_payload + def create(self, object_type, template_attribute, credential=None): return self._create(object_type=object_type, template_attribute=template_attribute, diff --git a/kmip/tests/unit/pie/test_client.py b/kmip/tests/unit/pie/test_client.py index 9309cbc..6879d42 100644 --- a/kmip/tests/unit/pie/test_client.py +++ b/kmip/tests/unit/pie/test_client.py @@ -24,6 +24,7 @@ from kmip.core import objects as obj from kmip.core.factories import attributes from kmip.core.messages import contents +from kmip.core.messages import payloads from kmip.core.primitives import DateTime from kmip.services.kmip_client import KMIPProxy @@ -758,6 +759,41 @@ class TestProxyKmipClient(testtools.TestCase): KmipOperationFailure, error_msg, client.create_key_pair, *args) + @mock.patch( + "kmip.pie.client.KMIPProxy", + mock.MagicMock(spec_set=KMIPProxy) + ) + def test_delete_attribute(self): + """ + Test that the client can delete an attribute. + """ + request_payload = payloads.DeleteAttributeRequestPayload( + unique_identifier="1", + attribute_name="Object Group", + attribute_index=2 + ) + response_payload = payloads.DeleteAttributeResponsePayload( + unique_identifier="1", + attribute=None + ) + + with ProxyKmipClient() as client: + client.proxy.send_request_payload.return_value = response_payload + + unique_identifier, attribute = client.delete_attribute( + "1", + attribute_name="Object Group", + attribute_index=2 + ) + + args = ( + enums.Operation.DELETE_ATTRIBUTE, + request_payload + ) + client.proxy.send_request_payload.assert_called_with(*args) + self.assertEqual("1", unique_identifier) + self.assertIsNone(attribute) + @mock.patch( 'kmip.pie.client.KMIPProxy', mock.MagicMock(spec_set=KMIPProxy) ) diff --git a/kmip/tests/unit/services/test_kmip_client.py b/kmip/tests/unit/services/test_kmip_client.py index 172dc49..51f0847 100644 --- a/kmip/tests/unit/services/test_kmip_client.py +++ b/kmip/tests/unit/services/test_kmip_client.py @@ -30,6 +30,8 @@ from kmip.core.enums import QueryFunction as QueryFunctionEnum from kmip.core.enums import CryptographicAlgorithm as \ CryptographicAlgorithmEnum +from kmip.core import exceptions + from kmip.core.factories.attributes import AttributeFactory from kmip.core.factories.credentials import CredentialFactory from kmip.core.factories.secrets import SecretFactory @@ -772,6 +774,238 @@ class TestKMIPClient(TestCase): self.client._create_socket(sock) self.assertEqual(ssl.SSLSocket, type(self.client.socket)) + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._build_request_message" + ) + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._send_and_receive_message" + ) + def test_send_request_payload(self, send_mock, build_mock): + """ + Test that the client can send a request payload and correctly handle + the resulting response messsage. + """ + request_payload = payloads.DeleteAttributeRequestPayload( + unique_identifier="1", + attribute_name="Object Group", + attribute_index=2 + ) + response_payload = payloads.DeleteAttributeResponsePayload( + unique_identifier="1", + attribute=None + ) + + batch_item = ResponseBatchItem( + operation=Operation(OperationEnum.DELETE_ATTRIBUTE), + result_status=ResultStatus(ResultStatusEnum.SUCCESS), + response_payload=response_payload + ) + response_message = ResponseMessage(batch_items=[batch_item]) + + build_mock.return_value = None + send_mock.return_value = response_message + + result = self.client.send_request_payload( + OperationEnum.DELETE_ATTRIBUTE, + request_payload + ) + + self.assertIsInstance(result, payloads.DeleteAttributeResponsePayload) + self.assertEqual(result, response_payload) + + def test_send_request_payload_invalid_payload(self): + """ + Test that a TypeError is raised when an invalid payload is used to + send a request. + """ + args = (OperationEnum.DELETE_ATTRIBUTE, "invalid") + self.assertRaisesRegex( + TypeError, + "The request payload must be a RequestPayload object.", + self.client.send_request_payload, + *args + ) + + def test_send_request_payload_mismatch_operation_payload(self): + """ + Test that a TypeError is raised when the operation and request payload + do not match up when used to send a request. + """ + args = ( + OperationEnum.DELETE_ATTRIBUTE, + payloads.CreateRequestPayload() + ) + self.assertRaisesRegex( + TypeError, + "The request payload for the DeleteAttribute operation must be a " + "DeleteAttributeRequestPayload object.", + self.client.send_request_payload, + *args + ) + + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._build_request_message" + ) + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._send_and_receive_message" + ) + def test_send_request_payload_incorrect_number_of_batch_items( + self, + send_mock, + build_mock + ): + """ + Test that an InvalidMessage error is raised when the wrong number of + response payloads are returned from the server. + """ + build_mock.return_value = None + send_mock.return_value = ResponseMessage(batch_items=[]) + + args = ( + OperationEnum.DELETE_ATTRIBUTE, + payloads.DeleteAttributeRequestPayload( + unique_identifier="1", + attribute_name="Object Group", + attribute_index=2 + ) + ) + + self.assertRaisesRegex( + exceptions.InvalidMessage, + "The response message does not have the right number of requested " + "operation results.", + self.client.send_request_payload, + *args + ) + + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._build_request_message" + ) + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._send_and_receive_message" + ) + def test_send_request_payload_mismatch_response_operation( + self, + send_mock, + build_mock + ): + """ + Test that an InvalidMessage error is raised when the wrong operation + is returned from the server. + """ + response_payload = payloads.DeleteAttributeResponsePayload( + unique_identifier="1", + attribute=None + ) + + batch_item = ResponseBatchItem( + operation=Operation(OperationEnum.CREATE), + result_status=ResultStatus(ResultStatusEnum.SUCCESS), + response_payload=response_payload + ) + build_mock.return_value = None + send_mock.return_value = ResponseMessage(batch_items=[batch_item]) + + args = ( + OperationEnum.DELETE_ATTRIBUTE, + payloads.DeleteAttributeRequestPayload( + unique_identifier="1", + attribute_name="Object Group", + attribute_index=2 + ) + ) + + self.assertRaisesRegex( + exceptions.InvalidMessage, + "The response message does not match the request operation.", + self.client.send_request_payload, + *args + ) + + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._build_request_message" + ) + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._send_and_receive_message" + ) + def test_send_request_payload_mismatch_response_payload( + self, + send_mock, + build_mock + ): + """ + Test that an InvalidMessage error is raised when the wrong payload + is returned from the server. + """ + response_payload = payloads.DestroyResponsePayload( + unique_identifier="1" + ) + + batch_item = ResponseBatchItem( + operation=Operation(OperationEnum.DELETE_ATTRIBUTE), + result_status=ResultStatus(ResultStatusEnum.SUCCESS), + response_payload=response_payload + ) + build_mock.return_value = None + send_mock.return_value = ResponseMessage(batch_items=[batch_item]) + + args = ( + OperationEnum.DELETE_ATTRIBUTE, + payloads.DeleteAttributeRequestPayload( + unique_identifier="1", + attribute_name="Object Group", + attribute_index=2 + ) + ) + + self.assertRaisesRegex( + exceptions.InvalidMessage, + "Invalid response payload received for the DeleteAttribute " + "operation.", + self.client.send_request_payload, + *args + ) + + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._build_request_message" + ) + @mock.patch( + "kmip.services.kmip_client.KMIPProxy._send_and_receive_message" + ) + def test_send_request_payload_operation_failure( + self, + send_mock, + build_mock + ): + """ + Test that a KmipOperationFailure error is raised when a payload + with a failure status is returned. + """ + batch_item = ResponseBatchItem( + operation=Operation(OperationEnum.DELETE_ATTRIBUTE), + result_status=ResultStatus(ResultStatusEnum.OPERATION_FAILED), + result_reason=ResultReason(ResultReasonEnum.GENERAL_FAILURE), + result_message=ResultMessage("Test failed!") + ) + build_mock.return_value = None + send_mock.return_value = ResponseMessage(batch_items=[batch_item]) + + args = ( + OperationEnum.DELETE_ATTRIBUTE, + payloads.DeleteAttributeRequestPayload( + unique_identifier="1", + attribute_name="Object Group", + attribute_index=2 + ) + ) + + self.assertRaisesRegex( + exceptions.OperationFailure, + "Test failed!", + self.client.send_request_payload, + *args + ) + @mock.patch( 'kmip.services.kmip_client.KMIPProxy._build_request_message' )