From da03fc5cf0774f4bcd884279356452deeac4e700 Mon Sep 17 00:00:00 2001 From: Mauricio Harley Date: Mon, 30 Jan 2023 17:29:08 +0100 Subject: Added secret consumers list functionality. Unit, smoke and functional tests were also added. Change-Id: I093475833cdc6d68ff2d4735a0d4a8d0eb143a53 --- barbicanclient/barbican_cli/v1/secrets.py | 24 ++++ barbicanclient/tests/v1/test_consumers.py | 32 +++++ barbicanclient/v1/secrets.py | 67 ++++++++++ .../cli/v1/behaviors/consumer_behaviors.py | 14 ++ functionaltests/cli/v1/smoke/test_consumer.py | 143 +++++++++++++++++++++ .../client/v1/functional/test_secrets.py | 139 +++++++++++++------- .../add-secret-consumers-a65cd6b22d28184d.yaml | 10 ++ setup.cfg | 1 + 8 files changed, 387 insertions(+), 43 deletions(-) create mode 100644 functionaltests/cli/v1/smoke/test_consumer.py create mode 100644 releasenotes/notes/add-secret-consumers-a65cd6b22d28184d.yaml diff --git a/barbicanclient/barbican_cli/v1/secrets.py b/barbicanclient/barbican_cli/v1/secrets.py index 3ce2348..a036ff1 100644 --- a/barbicanclient/barbican_cli/v1/secrets.py +++ b/barbicanclient/barbican_cli/v1/secrets.py @@ -252,3 +252,27 @@ class DeleteConsumer(command.Command): args.service_type_name, args.resource_type, args.resource_id) + + +class ListConsumer(lister.Lister): + """List consumers of a secret.""" + + def get_parser(self, prog_name): + parser = super(ListConsumer, self).get_parser(prog_name) + parser.add_argument('URI', help='The URI reference for the secret') + parser.add_argument('--limit', '-l', default=10, + help='specify the limit to the number of items ' + 'to list per page (default: %(default)s; ' + 'maximum: 100)', + type=int) + parser.add_argument('--offset', '-o', default=0, + help='specify the page offset ' + '(default: %(default)s)', + type=int) + return parser + + def take_action(self, args): + obj_list = self.app.client_manager.key_manager.secrets.list_consumers( + secret_ref=args.URI, limit=args.limit, offset=args.offset) + + return secrets.SecretConsumers._list_objects(obj_list) diff --git a/barbicanclient/tests/v1/test_consumers.py b/barbicanclient/tests/v1/test_consumers.py index 7b0c400..989981e 100644 --- a/barbicanclient/tests/v1/test_consumers.py +++ b/barbicanclient/tests/v1/test_consumers.py @@ -127,3 +127,35 @@ class WhenTestingConsumers(test_client.BaseEntityResource): def test_should_delete_from_manager_without_consumers_and_force(self): self._delete_from_manager(self.entity_href, force=True) + + def _list_consumers(self, secret_ref, consumers=[]): + mock_get_secret_for_client(self.client, consumers) + return self.manager.list_consumers(secret_ref) + + def test_list_consumers_from_secret_without_consumers(self): + consumer_list = self._list_consumers(self.entity_href) + self.assertTrue(len(consumer_list) == 0) + + def test_list_consumers_from_secret_with_consumers(self): + consumers = [{'service': 'service_test1', + 'resource_type': 'type_test1', + 'resource_id': 'id_test1'}, + {'service': 'service_test2', + 'resource_type': 'type_test2', + 'resource_id': 'id_test2'}] + consumer_list = self._list_consumers(self.entity_href, consumers) + + for elem in range(len(consumers)): + self.assertTrue( + consumer_list[elem].service == + consumers[elem]['service']) + self.assertTrue( + consumer_list[elem].resource_type == + consumers[elem]['resource_type']) + self.assertTrue( + consumer_list[elem].resource_id == + consumers[elem]['resource_id']) + + def test_should_fail_list_consumers_invalid_secret(self): + self.assertRaises(ValueError, self.manager.list_consumers, + **{'secret_ref': '12345'}) diff --git a/barbicanclient/v1/secrets.py b/barbicanclient/v1/secrets.py index 9f3ab8f..ca3c249 100644 --- a/barbicanclient/v1/secrets.py +++ b/barbicanclient/v1/secrets.py @@ -44,6 +44,49 @@ def immutable_after_save(func): return wrapper +class SecretConsumersFormatter(formatter.EntityFormatter): + + columns = ("Service", + "Resource type", + "Resource id", + "Created" + ) + + def _get_formatted_data(self): + data = (self.service, + self.resource_type, + self.resource_id, + self.created + ) + return data + + +class SecretConsumers(SecretConsumersFormatter): + """Secrets consumers managed by Barbican + + Secrets might or might not have consumers. + """ + + def __init__(self, secret_ref, service, resource_type, resource_id, + created=None, updated=None, status=None): + + self.secret_ref = secret_ref + self.service = service + self.resource_type = resource_type + self.resource_id = resource_id + self.created = created + self.updated = updated + self.status = status + + def __repr__(self): + return ('SecretConsumers(secret_ref="{0}", service="{1}", ' + 'resource_type="{2}", resource_id="{3}", ' + 'created="{4}", updated="{5}", status="{6}")' + .format(self.secret_ref, self.service, + self.resource_type, self.resource_id, + self.created, self.updated, self.status)) + + class SecretFormatter(formatter.EntityFormatter): columns = ("Secret href", @@ -689,3 +732,27 @@ class SecretManager(base.BaseEntityManager): } self._api.delete(href, json=consumer_dict) + + def list_consumers(self, secret_ref, limit=10, offset=0): + """List consumers of the secret + + :param secret_ref: Full HATEOAS reference to a secret, or a UUID + :param limit: Max number of consumers returned + :param offset: Offset secrets to begin list + :raises barbicanclient.exceptions.HTTPAuthError: 401 Responses + :raises barbicanclient.exceptions.HTTPClientError: 4xx Responses + :raises barbicanclient.exceptions.HTTPServerError: 5xx Responses + """ + LOG.debug('Listing consumers of secret {0}'.format(secret_ref)) + self._enforce_microversion() + secret_uuid = base.validate_ref_and_return_uuid( + secret_ref, 'secret') + href = '{0}/{1}/consumers'.format(self._entity, secret_uuid) + + params = {'limit': limit, 'offset': offset} + response = self._api.get(href, params=params) + + return [ + SecretConsumers(secret_ref=secret_ref, **s) + for s in response.get('consumers', []) + ] diff --git a/functionaltests/cli/v1/behaviors/consumer_behaviors.py b/functionaltests/cli/v1/behaviors/consumer_behaviors.py index 14d5578..af7f0d6 100644 --- a/functionaltests/cli/v1/behaviors/consumer_behaviors.py +++ b/functionaltests/cli/v1/behaviors/consumer_behaviors.py @@ -46,3 +46,17 @@ class ConsumerBehaviors(base_behaviors.BaseBehaviors): argv.extend([secret_href]) stdout, stderr = self.issue_barbican_command(argv) + + def list_consumers(self, secret_href): + argv = ['secret', 'consumer', 'list'] + self.add_auth_and_endpoint(argv) + + argv.extend([secret_href]) + + stdout, stderr = self.issue_barbican_command(argv) + + if len(stderr) > 0 or stdout == '\n': + return [] + else: + consumers = self._prettytable_to_list(stdout) + return consumers diff --git a/functionaltests/cli/v1/smoke/test_consumer.py b/functionaltests/cli/v1/smoke/test_consumer.py new file mode 100644 index 0000000..72496b9 --- /dev/null +++ b/functionaltests/cli/v1/smoke/test_consumer.py @@ -0,0 +1,143 @@ +# Copyright 2022 Red Hat Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functionaltests.cli.base import CmdLineTestCase +from functionaltests.cli.v1.behaviors.consumer_behaviors import ( + ConsumerBehaviors) +from functionaltests.cli.v1.behaviors.secret_behaviors import SecretBehaviors +from functionaltests import utils +from testtools import testcase + + +@utils.parameterized_test_case +class ConsumerTestCase(CmdLineTestCase): + + def setUp(self): + super(ConsumerTestCase, self).setUp() + self.consumer_behaviors = ConsumerBehaviors() + self.secret_behaviors = SecretBehaviors() + + def tearDown(self): + super(ConsumerTestCase, self).tearDown() + self.secret_behaviors.delete_all_created_secrets() + + def _create_secret(self): + secret_href = self.secret_behaviors.store_secret() + secret = self.secret_behaviors.get_secret(secret_href) + return secret['Secret href'] + + def _create_secret_with_consumer(self, consumer): + secret_href = self._create_secret() + self.consumer_behaviors.register_consumer( + secret_href, consumer["service"], consumer["resource_type"], + consumer["resource_id"]) + return secret_href + + def _register_consumer(self, secret_href, consumer): + self.consumer_behaviors.register_consumer( + secret_href, consumer["service"], consumer["resource_type"], + consumer["resource_id"]) + + @testcase.attr('positive') + def test_register_consumer_on_empty_secret(self): + consumer = { + 'service': 'service', 'resource_type': 'type', + 'resource_id': 'id', 'created': 'created' + } + secret_href = self._create_secret_with_consumer(consumer) + + secret = self.consumer_behaviors.list_consumers(secret_href) + # Because "created" is non-deterministic, we need to assign + # its value before running the loop below. + secret[0]['Created'] = consumer['created'] + # The CLI's output is slighted different in terms of headers. + # So, we have to rename their keys to the consumer dictionary's keys. + for (k1, v1), (k2, v2) in zip(list(secret[0].items()), + consumer.items()): + secret[0][k2] = secret[0].pop(k1) + self.assertDictEqual(consumer, secret[0]) + + @testcase.attr('positive') + def test_register_duplicated_service_name_consumer(self): + consumer = { + 'service': 'service', 'resource_type': 'type', 'resource_id': 'id' + } + secret_href = self._create_secret_with_consumer(consumer) + second_consumer = { + 'service': 'service', 'resource_type': 'type2', + 'resource_id': 'id2' + } + self._register_consumer(secret_href, second_consumer) + self._register_consumer(secret_href, second_consumer) + consumers_list = self.consumer_behaviors.list_consumers(secret_href) + self.assertEqual(2, len(consumers_list)) + + @testcase.attr('positive') + def test_register_duplicated_resource_type_consumer(self): + consumer = { + 'service': 'service', 'resource_type': 'type', + 'resource_id': 'id' + } + secret_href = self._create_secret_with_consumer(consumer) + second_consumer = { + 'service': 'service2', 'resource_type': 'type', + 'resource_id': 'id2' + } + self._register_consumer(secret_href, second_consumer) + consumers_list = self.consumer_behaviors.list_consumers(secret_href) + self.assertEqual(2, len(consumers_list)) + + @testcase.attr('positive') + def test_register_duplicated_resource_id_consumer(self): + consumer = { + 'service': 'service', 'resource_type': 'type', 'resource_id': 'id' + } + secret_href = self._create_secret_with_consumer(consumer) + second_consumer = { + 'service': 'service2', 'resource_type': 'type2', + 'resource_id': 'id' + } + self._register_consumer(secret_href, second_consumer) + consumers_list = self.consumer_behaviors.list_consumers(secret_href) + self.assertEqual(2, len(consumers_list)) + + @testcase.attr('positive') + def test_remove_consumer(self): + consumer = { + 'service': 'service', 'resource_type': 'type', + 'resource_id': 'id' + } + secret_href = self._create_secret_with_consumer(consumer) + + self.consumer_behaviors.remove_consumer( + secret_href, consumer["service"], consumer["resource_type"], + consumer["resource_id"]) + + consumers = self.consumer_behaviors.list_consumers(secret_href) + self.assertEqual(0, len(consumers)) + + @testcase.attr('positive') + def test_list_consumer_secret_with_multiple_consumers(self): + first_consumer = { + 'service': 'service1', 'resource_type': 'type1', + 'resource_id': 'id1'} + secret_href = self._create_secret_with_consumer(first_consumer) + + second_consumer = { + 'service': 'service2', 'resource_type': 'type2', + 'resource_id': 'id2'} + self._register_consumer(secret_href, second_consumer) + + consumers = self.consumer_behaviors.list_consumers(secret_href) + self.assertEqual(2, len(consumers)) diff --git a/functionaltests/client/v1/functional/test_secrets.py b/functionaltests/client/v1/functional/test_secrets.py index 645094a..fbee75a 100644 --- a/functionaltests/client/v1/functional/test_secrets.py +++ b/functionaltests/client/v1/functional/test_secrets.py @@ -69,15 +69,21 @@ class SecretsTestCase(base.TestCase): self.cleanup.delete_all_entities() super(SecretsTestCase, self).tearDown() - @testcase.attr('positive') - def test_secret_create_defaults_check_content_types(self): - """Check that set content-type attribute is retained in metadata.""" - secret = self.barbicanclient.secrets.create( + def _create_test_secret(self): + """Helper module to create a secret withouth consumers""" + new_secret = self.barbicanclient.secrets.create( **secret_create_defaults_data) - secret_ref = self.cleanup.add_entity(secret) + secret_ref = self.cleanup.add_entity(new_secret) self.assertIsNotNone(secret_ref) + return secret_ref + + @testcase.attr('positive') + def test_secret_create_defaults_check_content_types(self): + """Check that set content-type attribute is retained in metadata.""" + secret_ref = self._create_test_secret() + resp = self.barbicanclient.secrets.get(secret_ref) content_types = resp.content_types self.assertIsNotNone(content_types) @@ -106,11 +112,7 @@ class SecretsTestCase(base.TestCase): By default, 'read' ACL settings are there for a secret. """ - test_model = self.barbicanclient.secrets.create( - **secret_create_defaults_data) - - secret_ref = self.cleanup.add_entity(test_model) - self.assertIsNotNone(secret_ref) + secret_ref = self._create_test_secret() secret_entity = self.barbicanclient.secrets.get(secret_ref) self.assertIsNotNone(secret_entity.acls) @@ -179,11 +181,7 @@ class SecretsTestCase(base.TestCase): in the register_consumers list, then remove each consumer in the remove_consumers list. """ - new_secret = self.barbicanclient.secrets.create( - **secret_create_defaults_data) - - secret_ref = self.cleanup.add_entity(new_secret) - self.assertIsNotNone(secret_ref) + secret_ref = self._create_test_secret() for consumer in register_consumers: secret = self.barbicanclient.secrets.register_consumer( @@ -231,11 +229,7 @@ class SecretsTestCase(base.TestCase): providing all of the required positional arguments (service, resource_type, resource_id). """ - new_secret = self.barbicanclient.secrets.create( - **secret_create_defaults_data) - - secret_ref = self.cleanup.add_entity(new_secret) - self.assertIsNotNone(secret_ref) + secret_ref = self._create_test_secret() for consumer in register_consumers: e = self.assertRaises( @@ -267,11 +261,7 @@ class SecretsTestCase(base.TestCase): providing all of the required positional arguments (service, resource_type, resource_id). """ - new_secret = self.barbicanclient.secrets.create( - **secret_create_defaults_data) - - secret_ref = self.cleanup.add_entity(new_secret) - self.assertIsNotNone(secret_ref) + secret_ref = self._create_test_secret() secret = self.barbicanclient.secrets.register_consumer( secret_ref, @@ -290,10 +280,7 @@ class SecretsTestCase(base.TestCase): @testcase.attr('positive') def test_secret_delete_without_consumers_no_force(self): - new_secret = self.barbicanclient.secrets.create( - **secret_create_defaults_data) - - secret_ref = self.cleanup.add_entity(new_secret) + secret_ref = self._create_test_secret() self.barbicanclient.secrets.delete(secret_ref, force=False) resp = self.barbicanclient.secrets.get(secret_ref) @@ -302,10 +289,7 @@ class SecretsTestCase(base.TestCase): @testcase.attr('positive') def test_secret_delete_without_consumers_with_force(self): - new_secret = self.barbicanclient.secrets.create( - **secret_create_defaults_data) - - secret_ref = self.cleanup.add_entity(new_secret) + secret_ref = self._create_test_secret() self.barbicanclient.secrets.delete(secret_ref, force=True) resp = self.barbicanclient.secrets.get(secret_ref) @@ -319,11 +303,7 @@ class SecretsTestCase(base.TestCase): Tries to delete a secret with consumers, but without providing the 'force' parameter. """ - new_secret = self.barbicanclient.secrets.create( - **secret_create_defaults_data) - - secret_ref = self.cleanup.add_entity(new_secret) - self.assertIsNotNone(secret_ref) + secret_ref = self._create_test_secret() secret = self.barbicanclient.secrets.register_consumer( secret_ref, @@ -346,11 +326,7 @@ class SecretsTestCase(base.TestCase): Tries to delete a secret with consumers, making the 'force' parameter equals True. """ - new_secret = self.barbicanclient.secrets.create( - **secret_create_defaults_data) - - secret_ref = self.cleanup.add_entity(new_secret) - self.assertIsNotNone(secret_ref) + secret_ref = self._create_test_secret() secret = self.barbicanclient.secrets.register_consumer( secret_ref, @@ -365,6 +341,83 @@ class SecretsTestCase(base.TestCase): self.assertRaises(exceptions.HTTPClientError, getattr, resp, "name") self.cleanup.delete_entity(secret_ref) + @testcase.attr('positive') + def test_consumers_list_secret_without_consumers(self): + """Lists consumers from a secret without consumers""" + secret_ref = self._create_test_secret() + + consumers_list = self.barbicanclient.secrets.list_consumers( + secret_ref) + self.assertTrue(len(consumers_list) == 0) + + self.cleanup.delete_entity(secret_ref) + self.barbicanclient.secrets.delete(secret_ref, True) + + @testcase.attr('positive') + def test_consumers_list_secret_with_consumers(self): + """Lists consumers from a secret with consumers""" + secret_ref = self._create_test_secret() + + consumers = [{ + 'service': 'service1', + 'resource_type': 'type1', + 'resource_id': 'id1'}, { + 'service': 'service2', + 'resource_type': 'type2', + 'resource_id': 'id2'}] + + for consumer in consumers: + _ = self.barbicanclient.secrets.register_consumer( + secret_ref, + service=consumer['service'], + resource_type=consumer['resource_type'], + resource_id=consumer['resource_id'] + ) + + consumers_list = self.barbicanclient.secrets.list_consumers( + secret_ref) + + for elem in range(len(consumers)): + self.assertTrue( + consumers_list[elem].service == + consumers[elem]['service']) + self.assertTrue( + consumers_list[elem].resource_type == + consumers[elem]['resource_type']) + self.assertTrue( + consumers_list[elem].resource_id == + consumers[elem]['resource_id']) + + self.cleanup.delete_entity(secret_ref) + self.barbicanclient.secrets.delete(secret_ref, True) + + @testcase.attr('negative') + def test_consumers_list_secret_doesnt_exist(self): + """Tries to list consumers from a non-existent secret""" + e = self.assertRaises(exceptions.HTTPClientError, + self.barbicanclient.secrets.list_consumers, + '9999999f-f99f-49f9-9fff-f99f999ff9ff') + + self.assertIn("Secret not found", str(e)) + + @testcase.attr('negative') + def test_consumers_list_secret_invalid_uuid(self): + """Tries to list consumers providing an invalid secret UUID""" + e = self.assertRaises(exceptions.HTTPClientError, + self.barbicanclient.secrets.list_consumers, + '9999999f-ffff-ffff-9fff-f99f999ff9ff') + + self.assertIn("Provided secret id is invalid.", str(e)) + + @testcase.attr('negative') + def test_consumers_list_invalid_secret(self): + """Tries to list consumers providing an invalid secret""" + e = self.assertRaises(ValueError, + self.barbicanclient.secrets.list_consumers, + 'abcde') + + self.assertIn("secret incorrectly specified.", str(e)) + @testcase.attr('negative') def test_secret_delete_doesnt_exist(self): """Deletes a non-existent secret. diff --git a/releasenotes/notes/add-secret-consumers-a65cd6b22d28184d.yaml b/releasenotes/notes/add-secret-consumers-a65cd6b22d28184d.yaml new file mode 100644 index 0000000..443e735 --- /dev/null +++ b/releasenotes/notes/add-secret-consumers-a65cd6b22d28184d.yaml @@ -0,0 +1,10 @@ +--- +features: > + The Barbican API has been extended to allow secrets to have one or + more consumers. This extension has been documented here: + https://docs.openstack.org/barbican/latest/api/reference/secret_consumers.html + + This functionality has now been exposed in the barbican client. + Users may add, remove or delete consumers by calling new mechods on the + SecretManager. In addition, new CLI options have been provided to add, remove + and list consumers. See the documentation for details. diff --git a/setup.cfg b/setup.cfg index 52237fe..28f053d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,7 @@ openstack.key_manager.v1 = secret_consumer_create = barbicanclient.barbican_cli.v1.secrets:CreateConsumer secret_consumer_delete = barbicanclient.barbican_cli.v1.secrets:DeleteConsumer + secret_consumer_list = barbicanclient.barbican_cli.v1.secrets:ListConsumer ca_get = barbicanclient.barbican_cli.v1.cas:GetCA ca_list = barbicanclient.barbican_cli.v1.cas:ListCA -- cgit v1.2.1