diff --git a/doc/source/cli/command-objects/endpoint.rst b/doc/source/cli/command-objects/endpoint.rst index 02a75bea1..030947c22 100644 --- a/doc/source/cli/command-objects/endpoint.rst +++ b/doc/source/cli/command-objects/endpoint.rst @@ -4,6 +4,34 @@ endpoint Identity v2, v3 +endpoint add project +-------------------- + +Associate a project to and endpoint for endpoint filtering + +.. program:: endpoint add project +.. code:: bash + + openstack endpoint add project + [--project-domain ] + + + +.. option:: --project-domain + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. _endpoint_add_project-endpoint: +.. describe:: + + Endpoint to associate with specified project (name or ID) + +.. _endpoint_add_project-project: +.. describe:: + + Project to associate with specified endpoint (name or ID) + endpoint create --------------- @@ -107,6 +135,8 @@ List endpoints [--interface ] [--region ] [--long] + [--endpoint | + --project [--project-domain ]] .. option:: --service @@ -132,6 +162,55 @@ List endpoints *Identity version 2 only* +.. option:: --endpoint + + List projects that have access to that endpoint using + endpoint filtering + + *Identity version 3 only* + +.. option:: --project + + List endpoints available for the project using + endpoint filtering + + *Identity version 3 only* + +.. option:: --project-domain + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + + *Identity version 3 only* + +endpoint remove project +----------------------- + +Dissociate a project from an endpoint. + +.. program:: endpoint remove project +.. code:: bash + + openstack endpoint remove project + [--project-domain ] + + + +.. option:: --project-domain + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. _endpoint_remove_project-endpoint: +.. describe:: + + Endpoint to dissociate with specified project (name or ID) + +.. _endpoint_remove_project-project: +.. describe:: + + Project to dissociate with specified endpoint (name or ID) + endpoint set ------------ diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 3b4dd0de4..3229240e4 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -36,6 +36,42 @@ def get_service_name(service): return '' +class AddProjectToEndpoint(command.Command): + _description = _("Associate a project to an endpoint") + + def get_parser(self, prog_name): + parser = super( + AddProjectToEndpoint, self).get_parser(prog_name) + parser.add_argument( + 'endpoint', + metavar='', + help=_('Endpoint to associate with ' + 'specified project (name or ID)'), + ) + parser.add_argument( + 'project', + metavar='', + help=_('Project to associate with ' + 'specified endpoint name or ID)'), + ) + common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.identity + + endpoint = utils.find_resource(client.endpoints, + parsed_args.endpoint) + + project = common.find_project(client, + parsed_args.project, + parsed_args.project_domain) + + client.endpoint_filter.add_endpoint_to_project( + project=project.id, + endpoint=endpoint.id) + + class CreateEndpoint(command.ShowOne): _description = _("Create new endpoint") @@ -152,27 +188,68 @@ class ListEndpoint(command.Lister): metavar='', help=_('Filter by region ID'), ) + list_group = parser.add_mutually_exclusive_group() + list_group.add_argument( + '--endpoint', + metavar='', + help=_('Endpoint to list filters'), + ) + list_group.add_argument( + '--project', + metavar='', + help=_('Project to list filters (name or ID)'), + ) + common.add_project_domain_option_to_parser(list_group) return parser def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - columns = ('ID', 'Region', 'Service Name', 'Service Type', - 'Enabled', 'Interface', 'URL') - kwargs = {} - if parsed_args.service: - service = common.find_service(identity_client, parsed_args.service) - kwargs['service'] = service.id - if parsed_args.interface: - kwargs['interface'] = parsed_args.interface - if parsed_args.region: - kwargs['region'] = parsed_args.region - data = identity_client.endpoints.list(**kwargs) - service_list = identity_client.services.list() - for ep in data: - service = common.find_service_in_list(service_list, ep.service_id) - ep.service_name = get_service_name(service) - ep.service_type = service.type + endpoint = None + if parsed_args.endpoint: + endpoint = utils.find_resource(identity_client.endpoints, + parsed_args.endpoint) + project = None + if parsed_args.project: + project = common.find_project(identity_client, + parsed_args.project, + parsed_args.project_domain) + + if endpoint: + columns = ('ID', 'Name') + data = ( + identity_client.endpoint_filter + .list_projects_for_endpoint(endpoint=endpoint.id) + ) + else: + columns = ('ID', 'Region', 'Service Name', 'Service Type', + 'Enabled', 'Interface', 'URL') + kwargs = {} + if parsed_args.service: + service = common.find_service(identity_client, + parsed_args.service) + kwargs['service'] = service.id + if parsed_args.interface: + kwargs['interface'] = parsed_args.interface + if parsed_args.region: + kwargs['region'] = parsed_args.region + + if project: + data = ( + identity_client.endpoint_filter + .list_endpoints_for_project(project=project.id) + ) + else: + data = identity_client.endpoints.list(**kwargs) + + service_list = identity_client.services.list() + + for ep in data: + service = common.find_service_in_list(service_list, + ep.service_id) + ep.service_name = get_service_name(service) + ep.service_type = service.type + return (columns, (utils.get_item_properties( s, columns, @@ -180,6 +257,42 @@ class ListEndpoint(command.Lister): ) for s in data)) +class RemoveProjectFromEndpoint(command.Command): + _description = _("Dissociate a project from an endpoint") + + def get_parser(self, prog_name): + parser = super( + RemoveProjectFromEndpoint, self).get_parser(prog_name) + parser.add_argument( + 'endpoint', + metavar='', + help=_('Endpoint to dissociate from ' + 'specified project (name or ID)'), + ) + parser.add_argument( + 'project', + metavar='', + help=_('Project to dissociate from ' + 'specified endpoint name or ID)'), + ) + common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.identity + + endpoint = utils.find_resource(client.endpoints, + parsed_args.endpoint) + + project = common.find_project(client, + parsed_args.project, + parsed_args.project_domain) + + client.endpoint_filter.delete_endpoint_from_project( + project=project.id, + endpoint=endpoint.id) + + class SetEndpoint(command.Command): _description = _("Set endpoint properties") diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 6d7896d8f..33cb5d860 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -42,6 +42,7 @@ class IdentityTests(base.TestCase): REGION_LIST_HEADERS = ['Region', 'Parent Region', 'Description'] ENDPOINT_LIST_HEADERS = ['ID', 'Region', 'Service Name', 'Service Type', 'Enabled', 'Interface', 'URL'] + ENDPOINT_LIST_PROJECT_HEADERS = ['ID', 'Name'] IDENTITY_PROVIDER_FIELDS = ['description', 'enabled', 'id', 'remote_ids', 'domain_id'] diff --git a/openstackclient/tests/functional/identity/v3/test_endpoint.py b/openstackclient/tests/functional/identity/v3/test_endpoint.py index 22dc1b659..41f0b4c80 100644 --- a/openstackclient/tests/functional/identity/v3/test_endpoint.py +++ b/openstackclient/tests/functional/identity/v3/test_endpoint.py @@ -42,6 +42,29 @@ class EndpointTests(common.IdentityTests): items = self.parse_listing(raw_output) self.assert_table_structure(items, self.ENDPOINT_LIST_HEADERS) + def test_endpoint_list_filter(self): + endpoint_id = self._create_dummy_endpoint(add_clean_up=False) + project_id = self._create_dummy_project(add_clean_up=False) + raw_output = self.openstack( + 'endpoint add project ' + '%(endpoint_id)s ' + '%(project_id)s' % { + 'project_id': project_id, + 'endpoint_id': endpoint_id}) + self.assertEqual(0, len(raw_output)) + raw_output = self.openstack( + 'endpoint list --endpoint %s' % endpoint_id) + self.assertIn(project_id, raw_output) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, + self.ENDPOINT_LIST_PROJECT_HEADERS) + + raw_output = self.openstack( + 'endpoint list --project %s' % project_id) + self.assertIn(endpoint_id, raw_output) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.ENDPOINT_LIST_HEADERS) + def test_endpoint_set(self): endpoint_id = self._create_dummy_endpoint() new_endpoint_url = data_utils.rand_url() @@ -65,3 +88,22 @@ class EndpointTests(common.IdentityTests): raw_output = self.openstack('endpoint show %s' % endpoint_id) items = self.parse_show(raw_output) self.assert_show_fields(items, self.ENDPOINT_FIELDS) + + def test_endpoint_add_remove_project(self): + endpoint_id = self._create_dummy_endpoint(add_clean_up=False) + project_id = self._create_dummy_project(add_clean_up=False) + raw_output = self.openstack( + 'endpoint add project ' + '%(endpoint_id)s ' + '%(project_id)s' % { + 'project_id': project_id, + 'endpoint_id': endpoint_id}) + self.assertEqual(0, len(raw_output)) + + raw_output = self.openstack( + 'endpoint remove project ' + '%(endpoint_id)s ' + '%(project_id)s' % { + 'project_id': project_id, + 'endpoint_id': endpoint_id}) + self.assertEqual(0, len(raw_output)) diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index 7de251524..3e2caf01d 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -493,6 +493,8 @@ class FakeIdentityv3Client(object): self.credentials.resource_class = fakes.FakeResource(None, {}) self.endpoints = mock.Mock() self.endpoints.resource_class = fakes.FakeResource(None, {}) + self.endpoint_filter = mock.Mock() + self.endpoint_filter.resource_class = fakes.FakeResource(None, {}) self.groups = mock.Mock() self.groups.resource_class = fakes.FakeResource(None, {}) self.oauth1 = mock.Mock() @@ -911,6 +913,31 @@ class FakeEndpoint(object): loaded=True) return endpoint + @staticmethod + def create_one_endpoint_filter(attrs=None): + """Create a fake endpoint project relationship. + + :param Dictionary attrs: + A dictionary with all attributes of endpoint filter + :return: + A FakeResource object with project, endpoint and so on + """ + attrs = attrs or {} + + # Set default attribute + endpoint_filter_info = { + 'project': 'project-id-' + uuid.uuid4().hex, + 'endpoint': 'endpoint-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes if there are some attributes set + endpoint_filter_info.update(attrs) + + endpoint_filter = fakes.FakeModel( + copy.deepcopy(endpoint_filter_info)) + + return endpoint_filter + class FakeService(object): """Fake one or more service.""" diff --git a/openstackclient/tests/unit/identity/v3/test_endpoint.py b/openstackclient/tests/unit/identity/v3/test_endpoint.py index fad53fcb8..bfe930d6c 100644 --- a/openstackclient/tests/unit/identity/v3/test_endpoint.py +++ b/openstackclient/tests/unit/identity/v3/test_endpoint.py @@ -22,11 +22,23 @@ class TestEndpoint(identity_fakes.TestIdentityv3): # Get a shortcut to the EndpointManager Mock self.endpoints_mock = self.app.client_manager.identity.endpoints self.endpoints_mock.reset_mock() + self.ep_filter_mock = ( + self.app.client_manager.identity.endpoint_filter + ) + self.ep_filter_mock.reset_mock() # Get a shortcut to the ServiceManager Mock self.services_mock = self.app.client_manager.identity.services self.services_mock.reset_mock() + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + class TestEndpointCreate(TestEndpoint): @@ -750,3 +762,130 @@ class TestEndpointShowServiceWithoutName(TestEndpointShow): # Get the command object to test self.cmd = endpoint.ShowEndpoint(self.app, None) + + +class TestAddProjectToEndpoint(TestEndpoint): + + project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() + service = identity_fakes.FakeService.create_one_service() + endpoint = identity_fakes.FakeEndpoint.create_one_endpoint( + attrs={'service_id': service.id}) + + new_ep_filter = identity_fakes.FakeEndpoint.create_one_endpoint_filter( + attrs={'endpoint': endpoint.id, + 'project': project.id} + ) + + def setUp(self): + super(TestAddProjectToEndpoint, self).setUp() + + # This is the return value for utils.find_resource() + self.endpoints_mock.get.return_value = self.endpoint + + # Update the image_id in the MEMBER dict + self.ep_filter_mock.create.return_value = self.new_ep_filter + self.projects_mock.get.return_value = self.project + self.domains_mock.get.return_value = self.domain + # Get the command object to test + self.cmd = endpoint.AddProjectToEndpoint(self.app, None) + + def test_add_project_to_endpoint_no_option(self): + arglist = [ + self.endpoint.id, + self.project.id, + ] + verifylist = [ + ('endpoint', self.endpoint.id), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.ep_filter_mock.add_endpoint_to_project.assert_called_with( + project=self.project.id, + endpoint=self.endpoint.id + ) + self.assertIsNone(result) + + def test_add_project_to_endpoint_with_option(self): + arglist = [ + self.endpoint.id, + self.project.id, + '--project-domain', self.domain.id, + ] + verifylist = [ + ('endpoint', self.endpoint.id), + ('project', self.project.id), + ('project_domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.ep_filter_mock.add_endpoint_to_project.assert_called_with( + project=self.project.id, + endpoint=self.endpoint.id + ) + self.assertIsNone(result) + + +class TestRemoveProjectEndpoint(TestEndpoint): + + project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() + service = identity_fakes.FakeService.create_one_service() + endpoint = identity_fakes.FakeEndpoint.create_one_endpoint( + attrs={'service_id': service.id}) + + def setUp(self): + super(TestRemoveProjectEndpoint, self).setUp() + + # This is the return value for utils.find_resource() + self.endpoints_mock.get.return_value = self.endpoint + + self.projects_mock.get.return_value = self.project + self.domains_mock.get.return_value = self.domain + self.ep_filter_mock.delete.return_value = None + + # Get the command object to test + self.cmd = endpoint.RemoveProjectFromEndpoint(self.app, None) + + def test_remove_project_endpoint_no_options(self): + arglist = [ + self.endpoint.id, + self.project.id, + ] + verifylist = [ + ('endpoint', self.endpoint.id), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.ep_filter_mock.delete_endpoint_from_project.assert_called_with( + project=self.project.id, + endpoint=self.endpoint.id, + ) + self.assertIsNone(result) + + def test_remove_project_endpoint_with_options(self): + arglist = [ + self.endpoint.id, + self.project.id, + '--project-domain', self.domain.id, + ] + verifylist = [ + ('endpoint', self.endpoint.id), + ('project', self.project.id), + ('project_domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.ep_filter_mock.delete_endpoint_from_project.assert_called_with( + project=self.project.id, + endpoint=self.endpoint.id, + ) + self.assertIsNone(result) diff --git a/releasenotes/notes/keystone-endpoint-filter-e930a7b72276fa2c.yaml b/releasenotes/notes/keystone-endpoint-filter-e930a7b72276fa2c.yaml new file mode 100644 index 000000000..5a633eed8 --- /dev/null +++ b/releasenotes/notes/keystone-endpoint-filter-e930a7b72276fa2c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``endpoint add project``, ``endpoint remove project`` and ``endpoint + list`` commands to manage endpoint filters in identity v3. diff --git a/setup.cfg b/setup.cfg index e7b9cb6a2..63bfdafb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -228,11 +228,13 @@ openstack.identity.v3 = ec2_credentials_list = openstackclient.identity.v3.ec2creds:ListEC2Creds ec2_credentials_show = openstackclient.identity.v3.ec2creds:ShowEC2Creds + endpoint_add_project = openstackclient.identity.v3.endpoint:AddProjectToEndpoint endpoint_create = openstackclient.identity.v3.endpoint:CreateEndpoint endpoint_delete = openstackclient.identity.v3.endpoint:DeleteEndpoint + endpoint_list = openstackclient.identity.v3.endpoint:ListEndpoint + endpoint_remove_project = openstackclient.identity.v3.endpoint:RemoveProjectFromEndpoint endpoint_set = openstackclient.identity.v3.endpoint:SetEndpoint endpoint_show = openstackclient.identity.v3.endpoint:ShowEndpoint - endpoint_list = openstackclient.identity.v3.endpoint:ListEndpoint group_add_user = openstackclient.identity.v3.group:AddUserToGroup group_contains_user = openstackclient.identity.v3.group:CheckUserInGroup