From 2cc2f1081f3a2762ec469bd4b3cc2ed46fe0b73e Mon Sep 17 00:00:00 2001 From: Enrique Garcia Navalon Date: Wed, 13 May 2015 15:00:10 +0200 Subject: [PATCH] Add support for endpoint group filtering The following API calls are made available: - GET /OS-EP-FILTER/projects/{project_id}/endpoint_groups - GET /OS-EP-FILTER/endpoint_groups/{endpoint_group_id}/projects - PUT /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/{project_id} - HEAD /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/{project_id} - DELETE /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/{project_id} Co-Authored-By: Samuel de Medeiros Queiroz Closes-Bug: #1641674 Change-Id: Idf938267479b5b8c50c9aa141c3c2770c2d69839 --- .../functional/v3/test_endpoint_filters.py | 86 +++++++++++ .../functional/v3/test_endpoint_groups.py | 5 +- .../tests/functional/v3/test_projects.py | 21 +-- .../tests/unit/v3/test_endpoint_filter.py | 144 ++++++++++++++++++ keystoneclient/v3/contrib/endpoint_filter.py | 77 +++++++++- .../notes/bug-1641674-4862454115265e76.yaml | 8 + 6 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 keystoneclient/tests/functional/v3/test_endpoint_filters.py create mode 100644 releasenotes/notes/bug-1641674-4862454115265e76.yaml diff --git a/keystoneclient/tests/functional/v3/test_endpoint_filters.py b/keystoneclient/tests/functional/v3/test_endpoint_filters.py new file mode 100644 index 00000000..d8956bed --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_endpoint_filters.py @@ -0,0 +1,86 @@ +# 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 keystoneauth1.exceptions import http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures +from keystoneclient.tests.functional.v3 import test_endpoint_groups +from keystoneclient.tests.functional.v3 import test_projects + + +class EndpointFiltersTestCase(base.V3ClientTestCase, + test_endpoint_groups.EndpointGroupsTestMixin, + test_projects.ProjectsTestMixin): + + def setUp(self): + super(EndpointFiltersTestCase, self).setUp() + + self.project = fixtures.Project(self.client) + self.endpoint_group = fixtures.EndpointGroup(self.client) + self.useFixture(self.project) + self.useFixture(self.endpoint_group) + + self.client.endpoint_filter.add_endpoint_group_to_project( + self.endpoint_group, self.project) + + def test_add_endpoint_group_to_project(self): + project = fixtures.Project(self.client) + endpoint_group = fixtures.EndpointGroup(self.client) + self.useFixture(project) + self.useFixture(endpoint_group) + + self.client.endpoint_filter.add_endpoint_group_to_project( + endpoint_group, project) + self.client.endpoint_filter.check_endpoint_group_in_project( + endpoint_group, project) + + def test_delete_endpoint_group_from_project(self): + self.client.endpoint_filter.delete_endpoint_group_from_project( + self.endpoint_group, self.project) + self.assertRaises( + http.NotFound, + self.client.endpoint_filter.check_endpoint_group_in_project, + self.endpoint_group, self.project) + + def test_list_endpoint_groups_for_project(self): + endpoint_group_two = fixtures.EndpointGroup(self.client) + self.useFixture(endpoint_group_two) + self.client.endpoint_filter.add_endpoint_group_to_project( + endpoint_group_two, self.project) + + endpoint_groups = ( + self.client.endpoint_filter.list_endpoint_groups_for_project( + self.project + ) + ) + + for endpoint_group in endpoint_groups: + self.check_endpoint_group(endpoint_group) + + self.assertIn(self.endpoint_group.entity, endpoint_groups) + self.assertIn(endpoint_group_two.entity, endpoint_groups) + + def test_list_projects_for_endpoint_group(self): + project_two = fixtures.Project(self.client) + self.useFixture(project_two) + self.client.endpoint_filter.add_endpoint_group_to_project( + self.endpoint_group, project_two) + + f = self.client.endpoint_filter.list_projects_for_endpoint_group + projects = f(self.endpoint_group) + + for project in projects: + self.check_project(project) + + self.assertIn(self.project.entity, projects) + self.assertIn(project_two.entity, projects) diff --git a/keystoneclient/tests/functional/v3/test_endpoint_groups.py b/keystoneclient/tests/functional/v3/test_endpoint_groups.py index 10eccfb2..52fcf724 100644 --- a/keystoneclient/tests/functional/v3/test_endpoint_groups.py +++ b/keystoneclient/tests/functional/v3/test_endpoint_groups.py @@ -18,7 +18,7 @@ from keystoneclient.tests.functional import base from keystoneclient.tests.functional.v3 import client_fixtures as fixtures -class EndpointGroupsTestCase(base.V3ClientTestCase): +class EndpointGroupsTestMixin(object): def check_endpoint_group(self, endpoint_group, endpoint_group_ref=None): self.assertIsNotNone(endpoint_group.id) @@ -40,6 +40,9 @@ class EndpointGroupsTestCase(base.V3ClientTestCase): self.assertIsNotNone(endpoint_group.name) self.assertIsNotNone(endpoint_group.filters) + +class EndpointGroupsTestCase(base.V3ClientTestCase, EndpointGroupsTestMixin): + def test_create_endpoint_group(self): endpoint_group_ref = { 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, diff --git a/keystoneclient/tests/functional/v3/test_projects.py b/keystoneclient/tests/functional/v3/test_projects.py index 0fa631d1..4b8d7493 100644 --- a/keystoneclient/tests/functional/v3/test_projects.py +++ b/keystoneclient/tests/functional/v3/test_projects.py @@ -18,15 +18,7 @@ from keystoneclient.tests.functional import base from keystoneclient.tests.functional.v3 import client_fixtures as fixtures -class ProjectsTestCase(base.V3ClientTestCase): - - def setUp(self): - super(ProjectsTestCase, self).setUp() - self.test_domain = fixtures.Domain(self.client) - self.useFixture(self.test_domain) - - self.test_project = fixtures.Project(self.client, self.test_domain.id) - self.useFixture(self.test_project) +class ProjectsTestMixin(object): def check_project(self, project, project_ref=None): self.assertIsNotNone(project.id) @@ -51,6 +43,17 @@ class ProjectsTestCase(base.V3ClientTestCase): self.assertIsNotNone(project.domain_id) self.assertIsNotNone(project.enabled) + +class ProjectsTestCase(base.V3ClientTestCase, ProjectsTestMixin): + + def setUp(self): + super(ProjectsTestCase, self).setUp() + self.test_domain = fixtures.Domain(self.client) + self.useFixture(self.test_domain) + + self.test_project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(self.test_project) + def test_create_subproject(self): project_ref = { 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, diff --git a/keystoneclient/tests/unit/v3/test_endpoint_filter.py b/keystoneclient/tests/unit/v3/test_endpoint_filter.py index 2eed7058..62e89cb3 100644 --- a/keystoneclient/tests/unit/v3/test_endpoint_filter.py +++ b/keystoneclient/tests/unit/v3/test_endpoint_filter.py @@ -36,6 +36,13 @@ class EndpointTestUtils(object): kwargs.setdefault('url', uuid.uuid4().hex) return kwargs + def new_endpoint_group_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('filters') + return kwargs + class EndpointFilterTests(utils.ClientTestCase, EndpointTestUtils): """Test project-endpoint associations (a.k.a. EndpointFilter Extension). @@ -147,3 +154,140 @@ class EndpointFilterTests(utils.ClientTestCase, EndpointTestUtils): project['id'] for project in projects['projects']] actual_project_ids = [project.id for project in projects_resp] self.assertEqual(expected_project_ids, actual_project_ids) + + def test_list_projects_for_endpoint_group(self): + endpoint_group_id = uuid.uuid4().hex + projects = {'projects': [self.new_project_ref(), + self.new_project_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects'], + json=projects, + status_code=200) + + projects_resp = self.manager.list_projects_for_endpoint_group( + endpoint_group=endpoint_group_id) + + expected_project_ids = [ + project['id'] for project in projects['projects']] + actual_project_ids = [project.id for project in projects_resp] + self.assertEqual(expected_project_ids, actual_project_ids) + + def test_list_projects_for_endpoint_group_value_error(self): + self.assertRaises(ValueError, + self.manager.list_projects_for_endpoint_group, + endpoint_group='') + self.assertRaises(ValueError, + self.manager.list_projects_for_endpoint_group, + endpoint_group=None) + + def test_list_endpoint_groups_for_project(self): + project_id = uuid.uuid4().hex + endpoint_groups = { + 'endpoint_groups': [self.new_endpoint_group_ref(), + self.new_endpoint_group_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'projects', + project_id, 'endpoint_groups'], + json=endpoint_groups, + status_code=200) + + endpoint_groups_resp = self.manager.list_endpoint_groups_for_project( + project=project_id) + + expected_endpoint_group_ids = [ + endpoint_group['id'] for endpoint_group + in endpoint_groups['endpoint_groups'] + ] + actual_endpoint_group_ids = [ + endpoint_group.id for endpoint_group in endpoint_groups_resp + ] + self.assertEqual(expected_endpoint_group_ids, + actual_endpoint_group_ids) + + def test_list_endpoint_groups_for_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.list_endpoint_groups_for_project, + project=value) + + def test_add_endpoint_group_to_project(self): + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('PUT', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects', project_id], + status_code=201) + + self.manager.add_endpoint_group_to_project( + project=project_id, endpoint_group=endpoint_group_id) + + def test_add_endpoint_group_to_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.add_endpoint_group_to_project, + project=value, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.add_endpoint_group_to_project, + project=uuid.uuid4().hex, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.add_endpoint_group_to_project, + project=value, + endpoint_group=uuid.uuid4().hex) + + def test_check_endpoint_group_in_project(self): + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('HEAD', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects', project_id], + status_code=201) + + self.manager.check_endpoint_group_in_project( + project=project_id, endpoint_group=endpoint_group_id) + + def test_check_endpoint_group_in_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.check_endpoint_group_in_project, + project=value, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.check_endpoint_group_in_project, + project=uuid.uuid4().hex, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.check_endpoint_group_in_project, + project=value, + endpoint_group=uuid.uuid4().hex) + + def test_delete_endpoint_group_from_project(self): + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('DELETE', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects', project_id], + status_code=201) + + self.manager.delete_endpoint_group_from_project( + project=project_id, endpoint_group=endpoint_group_id) + + def test_delete_endpoint_group_from_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.delete_endpoint_group_from_project, + project=value, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.delete_endpoint_group_from_project, + project=uuid.uuid4().hex, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.delete_endpoint_group_from_project, + project=value, + endpoint_group=uuid.uuid4().hex) diff --git a/keystoneclient/v3/contrib/endpoint_filter.py b/keystoneclient/v3/contrib/endpoint_filter.py index 586a74a6..26d5a874 100644 --- a/keystoneclient/v3/contrib/endpoint_filter.py +++ b/keystoneclient/v3/contrib/endpoint_filter.py @@ -15,12 +15,18 @@ from keystoneclient import base from keystoneclient import exceptions from keystoneclient.i18n import _ +from keystoneclient.v3 import endpoint_groups from keystoneclient.v3 import endpoints from keystoneclient.v3 import projects class EndpointFilterManager(base.Manager): - """Manager class for manipulating project-endpoint associations.""" + """Manager class for manipulating project-endpoint associations. + + Project-endpoint associations can be with endpoints directly or via + endpoint groups. + + """ OS_EP_FILTER_EXT = '/OS-EP-FILTER' @@ -40,6 +46,23 @@ class EndpointFilterManager(base.Manager): return self.OS_EP_FILTER_EXT + api_path + def _build_group_base_url(self, project=None, endpoint_group=None): + project_id = base.getid(project) + endpoint_group_id = base.getid(endpoint_group) + + if project_id and endpoint_group_id: + api_path = '/endpoint_groups/%s/projects/%s' % ( + endpoint_group_id, project_id) + elif project_id: + api_path = '/projects/%s/endpoint_groups' % (project_id) + elif endpoint_group_id: + api_path = '/endpoint_groups/%s/projects' % (endpoint_group_id) + else: + msg = _('Must specify a project, an endpoint group, or both') + raise exceptions.ValidationError(msg) + + return self.OS_EP_FILTER_EXT + api_path + def add_endpoint_to_project(self, project, endpoint): """Create a project-endpoint association.""" if not (project and endpoint): @@ -59,7 +82,7 @@ class EndpointFilterManager(base.Manager): return super(EndpointFilterManager, self)._delete(url=base_url) def check_endpoint_in_project(self, project, endpoint): - """Check if project-endpoint association exist.""" + """Check if project-endpoint association exists.""" if not (project and endpoint): raise ValueError(_('project and endpoint are required')) @@ -88,3 +111,53 @@ class EndpointFilterManager(base.Manager): base_url, projects.ProjectManager.collection_key, obj_class=projects.ProjectManager.resource_class) + + def add_endpoint_group_to_project(self, endpoint_group, project): + """Create a project-endpoint group association.""" + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_group_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._put(url=base_url) + + def delete_endpoint_group_from_project(self, endpoint_group, project): + """Remove a project-endpoint group association.""" + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_group_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._delete(url=base_url) + + def check_endpoint_group_in_project(self, endpoint_group, project): + """Check if project-endpoint group association exists.""" + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_group_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._head(url=base_url) + + def list_endpoint_groups_for_project(self, project): + """List all endpoint groups for a given project.""" + if not project: + raise ValueError(_('project is required')) + + base_url = self._build_group_base_url(project=project) + + return super(EndpointFilterManager, self)._list( + base_url, + 'endpoint_groups', + obj_class=endpoint_groups.EndpointGroupManager.resource_class) + + def list_projects_for_endpoint_group(self, endpoint_group): + """List all projects associated with a given endpoint group.""" + if not endpoint_group: + raise ValueError(_('endpoint_group is required')) + + base_url = self._build_group_base_url(endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._list( + base_url, + projects.ProjectManager.collection_key, + obj_class=projects.ProjectManager.resource_class) diff --git a/releasenotes/notes/bug-1641674-4862454115265e76.yaml b/releasenotes/notes/bug-1641674-4862454115265e76.yaml new file mode 100644 index 00000000..19c8ecc3 --- /dev/null +++ b/releasenotes/notes/bug-1641674-4862454115265e76.yaml @@ -0,0 +1,8 @@ +--- +prelude: > + Keystone Client now supports endpoint group filtering. +features: + - | + Support for handling the relationship between endpoint groups and projects + has been added. It is now possible to list, associate, check and + disassociate endpoint groups that have access to a project.