From 4a6a2eccc12b6a9785ec77f1ca6998dc7b0351be Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Fri, 9 Aug 2019 16:37:39 -0400 Subject: [PATCH] Add offset and maximum item filtering for the Locate operation This change updates Locate operation support in the PyKMIP server, allowing users to filter objects using the offset and maximum item constraints. The offset constraint tells the server how many matching items should be skipped before results are returned. The maximum items constraint tells the server how many matching items should be returned. Unit tests and integration tests have been added to test and verify the correctness of this feature. Additionally, the Locate demo scripts have also been updated to support offset and maximum item filtering. Simply use the "--offset-items" and "--maximum-items" flags to specify offset and maximum item values for the Locate script to filter on. Fixes #562 --- kmip/demos/pie/locate.py | 15 ++- kmip/demos/units/locate.py | 16 ++- kmip/demos/utils.py | 16 +++ kmip/pie/client.py | 15 ++- kmip/services/kmip_client.py | 10 +- kmip/services/server/engine.py | 16 +++ .../integration/services/test_integration.py | 16 +++ .../services/test_proxykmipclient.py | 16 +++ .../tests/unit/services/server/test_engine.py | 98 +++++++++++++++++++ 9 files changed, 210 insertions(+), 8 deletions(-) diff --git a/kmip/demos/pie/locate.py b/kmip/demos/pie/locate.py index 10f3b50..f7908c6 100644 --- a/kmip/demos/pie/locate.py +++ b/kmip/demos/pie/locate.py @@ -32,6 +32,8 @@ if __name__ == '__main__': opts, args = parser.parse_args(sys.argv[1:]) config = opts.config + offset_items = opts.offset_items + maximum_items = opts.maximum_items name = opts.name initial_dates = opts.initial_dates state = opts.state @@ -43,6 +45,13 @@ if __name__ == '__main__': attribute_factory = AttributeFactory() + if offset_items and (offset_items < 0): + logger.error("Invalid offset items value provided.") + sys.exit(-1) + if maximum_items and (maximum_items < 0): + logger.error("Invalid maximum items value provided.") + sys.exit(-1) + # Build attributes if any are specified attributes = [] if name: @@ -159,7 +168,11 @@ if __name__ == '__main__': config_file=opts.config_file ) as client: try: - uuids = client.locate(attributes=attributes) + uuids = client.locate( + attributes=attributes, + offset_items=offset_items, + maximum_items=maximum_items + ) logger.info("Located uuids: {0}".format(uuids)) except Exception as e: logger.error(e) diff --git a/kmip/demos/units/locate.py b/kmip/demos/units/locate.py index 7f8c5ca..f46ee00 100644 --- a/kmip/demos/units/locate.py +++ b/kmip/demos/units/locate.py @@ -35,6 +35,8 @@ if __name__ == '__main__': username = opts.username password = opts.password config = opts.config + offset_items = opts.offset_items + maximum_items = opts.maximum_items name = opts.name initial_dates = opts.initial_dates state = opts.state @@ -62,6 +64,13 @@ if __name__ == '__main__': credential_value ) + if offset_items and (offset_items < 0): + logger.error("Invalid offset items value provided.") + sys.exit(-1) + if maximum_items and (maximum_items < 0): + logger.error("Invalid maximum items value provided.") + sys.exit(-1) + # Build the client and connect to the server client = kmip_client.KMIPProxy(config=config, config_file=opts.config_file) client.open() @@ -180,7 +189,12 @@ if __name__ == '__main__': ) ) - result = client.locate(attributes=attributes, credential=credential) + result = client.locate( + attributes=attributes, + offset_items=offset_items, + maximum_items=maximum_items, + credential=credential + ) client.close() # Display operation results diff --git a/kmip/demos/utils.py b/kmip/demos/utils.py index 6cd6a8d..f4308d8 100644 --- a/kmip/demos/utils.py +++ b/kmip/demos/utils.py @@ -230,6 +230,22 @@ def build_cli_parser(operation=None): help="List of attribute names to retrieve, defaults to all " "attributes") elif operation is Operation.LOCATE: + parser.add_option( + "--offset-items", + action="store", + type="int", + default=None, + dest="offset_items", + help="The number of matching secrets to skip." + ) + parser.add_option( + "--maximum-items", + action="store", + type="int", + default=None, + dest="maximum_items", + help="The maximum number of matching secrets to return." + ) parser.add_option( "-n", "--name", diff --git a/kmip/pie/client.py b/kmip/pie/client.py index 4d58615..2d23636 100644 --- a/kmip/pie/client.py +++ b/kmip/pie/client.py @@ -661,7 +661,7 @@ class ProxyKmipClient(object): @is_connected def locate(self, maximum_items=None, storage_status_mask=None, - object_group_member=None, attributes=None): + object_group_member=None, attributes=None, offset_items=None): """ Search for managed objects, depending on the attributes specified in the request. @@ -669,6 +669,8 @@ class ProxyKmipClient(object): Args: maximum_items (integer): Maximum number of object identifiers the server MAY return. + offset_items (integer): Number of object identifiers the server + should skip before returning results. storage_status_mask (integer): A bit mask that indicates whether on-line or archived objects are to be searched. object_group_member (ObjectGroupMember): An enumeration that @@ -688,6 +690,9 @@ class ProxyKmipClient(object): if maximum_items is not None: if not isinstance(maximum_items, six.integer_types): raise TypeError("maximum_items must be an integer") + if offset_items is not None: + if not isinstance(offset_items, six.integer_types): + raise TypeError("offset items must be an integer") if storage_status_mask is not None: if not isinstance(storage_status_mask, six.integer_types): raise TypeError("storage_status_mask must be an integer") @@ -705,8 +710,12 @@ class ProxyKmipClient(object): # Search for managed objects and handle the results result = self.proxy.locate( - maximum_items, storage_status_mask, - object_group_member, attributes) + maximum_items=maximum_items, + offset_items=offset_items, + storage_status_mask=storage_status_mask, + object_group_member=object_group_member, + attributes=attributes + ) status = result.result_status.value if status == enums.ResultStatus.SUCCESS: diff --git a/kmip/services/kmip_client.py b/kmip/services/kmip_client.py index f87e65a..38bbc44 100644 --- a/kmip/services/kmip_client.py +++ b/kmip/services/kmip_client.py @@ -694,11 +694,13 @@ class KMIPProxy(object): return results[0] def locate(self, maximum_items=None, storage_status_mask=None, - object_group_member=None, attributes=None, credential=None): + object_group_member=None, attributes=None, credential=None, + offset_items=None): return self._locate(maximum_items=maximum_items, storage_status_mask=storage_status_mask, object_group_member=object_group_member, - attributes=attributes, credential=credential) + attributes=attributes, credential=credential, + offset_items=offset_items) def query(self, batch=False, query_functions=None, credential=None): """ @@ -1476,12 +1478,14 @@ class KMIPProxy(object): return result def _locate(self, maximum_items=None, storage_status_mask=None, - object_group_member=None, attributes=None, credential=None): + object_group_member=None, attributes=None, credential=None, + offset_items=None): operation = Operation(OperationEnum.LOCATE) payload = payloads.LocateRequestPayload( maximum_items=maximum_items, + offset_items=offset_items, storage_status_mask=storage_status_mask, object_group_member=object_group_member, attributes=attributes diff --git a/kmip/services/server/engine.py b/kmip/services/server/engine.py index a64ae4f..73cc9e9 100644 --- a/kmip/services/server/engine.py +++ b/kmip/services/server/engine.py @@ -1780,6 +1780,22 @@ class KmipEngine(object): reverse=True ) + # Skip the requested offset items and keep the requested maximum items + if payload.offset_items is not None: + if payload.maximum_items is not None: + managed_objects = managed_objects[ + payload.offset_items:( + payload.offset_items + payload.maximum_items + ) + ] + else: + managed_objects = managed_objects[payload.offset_items:] + else: + if payload.maximum_items is not None: + managed_objects = managed_objects[:payload.maximum_items] + else: + pass + unique_identifiers = [ str(x.unique_identifier) for x in managed_objects ] diff --git a/kmip/tests/integration/services/test_integration.py b/kmip/tests/integration/services/test_integration.py index 631d2d3..eaceb69 100644 --- a/kmip/tests/integration/services/test_integration.py +++ b/kmip/tests/integration/services/test_integration.py @@ -1477,6 +1477,22 @@ class TestIntegration(testtools.TestCase): ) self.assertEqual(0, len(result.uuids)) + # Test locating keys using offset and maximum item constraints. + result = self.client.locate(offset_items=1) + + self.assertEqual(1, len(result.uuids)) + self.assertIn(uid_a, result.uuids) + + result = self.client.locate(maximum_items=1) + + self.assertEqual(1, len(result.uuids)) + self.assertIn(uid_b, result.uuids) + + result = self.client.locate(offset_items=1, maximum_items=1) + + self.assertEqual(1, len(result.uuids)) + self.assertIn(uid_a, result.uuids) + # Clean up keys result = self.client.destroy(uid_a) self.assertEqual(ResultStatus.SUCCESS, result.result_status.value) diff --git a/kmip/tests/integration/services/test_proxykmipclient.py b/kmip/tests/integration/services/test_proxykmipclient.py index 26e28b0..d38185a 100644 --- a/kmip/tests/integration/services/test_proxykmipclient.py +++ b/kmip/tests/integration/services/test_proxykmipclient.py @@ -1132,6 +1132,22 @@ class TestProxyKmipClientIntegration(testtools.TestCase): ) self.assertEqual(0, len(result)) + # Test locating keys using offset and maximum item constraints. + result = self.client.locate(offset_items=1) + + self.assertEqual(1, len(result)) + self.assertIn(a_id, result) + + result = self.client.locate(maximum_items=1) + + self.assertEqual(1, len(result)) + self.assertIn(b_id, result) + + result = self.client.locate(offset_items=1, maximum_items=1) + + self.assertEqual(1, len(result)) + self.assertIn(a_id, result) + # Clean up the keys self.client.destroy(a_id) self.client.destroy(b_id) diff --git a/kmip/tests/unit/services/server/test_engine.py b/kmip/tests/unit/services/server/test_engine.py index e96e302..d6dbb64 100644 --- a/kmip/tests/unit/services/server/test_engine.py +++ b/kmip/tests/unit/services/server/test_engine.py @@ -4474,6 +4474,104 @@ class TestKmipEngine(testtools.TestCase): self.assertIn(id_a, response_payload.unique_identifiers) self.assertIn(id_b, response_payload.unique_identifiers) + def test_locate_with_offset_and_maximum_items(self): + """ + Test locate operation with specified offset and maximum item limits. + """ + 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._is_allowed_by_operation_policy = mock.Mock(return_value=True) + e._logger = mock.MagicMock() + + key = ( + b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + ) + obj_a = pie_objects.SymmetricKey( + enums.CryptographicAlgorithm.AES, + 128, + key, + name='name1' + ) + obj_a.initial_date = int(time.time()) + time.sleep(2) + obj_b = pie_objects.SymmetricKey( + enums.CryptographicAlgorithm.DES, + 128, + key, + name='name2' + ) + obj_b.initial_date = int(time.time()) + time.sleep(2) + obj_c = pie_objects.SymmetricKey( + enums.CryptographicAlgorithm.AES, + 128, + key, + name='name3' + ) + obj_c.initial_date = int(time.time()) + + e._data_session.add(obj_a) + e._data_session.add(obj_b) + e._data_session.add(obj_c) + 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) + id_c = str(obj_c.unique_identifier) + + # Locate all objects. + payload = payloads.LocateRequestPayload() + e._logger.reset_mock() + response_payload = e._process_locate(payload) + e._data_session.commit() + e._data_session = e._data_store_session_factory() + + e._logger.info.assert_any_call("Processing operation: Locate") + + self.assertEqual( + [id_c, id_b, id_a], + response_payload.unique_identifiers + ) + + # Locate by skipping the first object and only returning one object. + payload = payloads.LocateRequestPayload( + offset_items=1, + maximum_items=1 + ) + e._logger.reset_mock() + response_payload = e._process_locate(payload) + e._data_session.commit() + e._data_session = e._data_store_session_factory() + + e._logger.info.assert_any_call("Processing operation: Locate") + + self.assertEqual([id_b], response_payload.unique_identifiers) + + # Locate by skipping the first two objects. + payload = payloads.LocateRequestPayload(offset_items=2) + e._logger.reset_mock() + response_payload = e._process_locate(payload) + e._data_session.commit() + e._data_session = e._data_store_session_factory() + + e._logger.info.assert_any_call("Processing operation: Locate") + + self.assertEqual([id_a], response_payload.unique_identifiers) + + # Locate by only returning two objects. + payload = payloads.LocateRequestPayload(maximum_items=2) + e._logger.reset_mock() + response_payload = e._process_locate(payload) + e._data_session.commit() + e._data_session = e._data_store_session_factory() + + e._logger.info.assert_any_call("Processing operation: Locate") + + self.assertEqual([id_c, id_b], response_payload.unique_identifiers) + def test_locate_with_name(self): """ Test locate operation when 'Name' attribute is given.