diff options
author | Rodrigo Duarte Sousa <rodrigods@lsd.ufcg.edu.br> | 2014-08-04 17:36:58 -0300 |
---|---|---|
committer | Rodrigo Duarte Sousa <rodrigods@lsd.ufcg.edu.br> | 2014-10-21 08:59:06 -0300 |
commit | a98858e17087a6a1f3862d1a38c66f164fa15279 (patch) | |
tree | 191ffe9949a34a05f6e9d5b838e9617d5982d975 | |
parent | d81f23a6c3abf882c20b7c3e86a6c5315d9fb834 (diff) | |
download | keystone-feature/hierarchical-multitenancy.tar.gz |
Base methods to handle hierarchical projectsfeature/hierarchical-multitenancy
Add base methods to be used at hierarhical projects.
Co-Authored-By: Rodrigo Duarte <rodrigods@lsd.ufcg.edu.br>
Co-Authored-By: Raildo Mascena <raildo@lsd.ufcg.edu.br>
Co-Authored-By: Telles Mota Vidal Nobrega <tellesmvn@lsd.ufcg.edu.br>
Co-Authored-By: Andre Aranha <afaranha@lsd.ufcg.edu.br>
Co-Authored-By: Samuel de Medeiros Queiroz <samuel@lsd.ufcg.edu.br>
Implements: blueprint hierarchical-multitenancy
Change-Id: I70ffbf426af5c3901dc2347efb9a43540f7d0604
-rw-r--r-- | keystone/assignment/backends/ldap.py | 15 | ||||
-rw-r--r-- | keystone/assignment/backends/sql.py | 57 | ||||
-rw-r--r-- | keystone/assignment/controllers.py | 3 | ||||
-rw-r--r-- | keystone/assignment/core.py | 39 | ||||
-rw-r--r-- | keystone/tests/test_backend.py | 116 | ||||
-rw-r--r-- | keystone/tests/test_backend_ldap.py | 9 |
6 files changed, 237 insertions, 2 deletions
diff --git a/keystone/assignment/backends/ldap.py b/keystone/assignment/backends/ldap.py index 64d587214..e5d455bdd 100644 --- a/keystone/assignment/backends/ldap.py +++ b/keystone/assignment/backends/ldap.py @@ -79,6 +79,21 @@ class Assignment(assignment.Driver): # any domain specified return self.list_projects(driver_hints.Hints()) + def list_projects_in_subtree(self, project_id): + # We don't support projects hierarchy within this driver, so a + # project will never have children + return [] + + def list_project_parents(self, project_id): + # We don't support projects hierarchy within this driver, so a + # project will never have parents + return [] + + def is_leaf_project(self, project_id): + # We don't support projects hierarchy within this driver, so a + # project will always be a root and a leaf at the same time + return True + def get_project_by_name(self, tenant_name, domain_id): self._validate_default_domain_id(domain_id) return self._set_default_attributes( diff --git a/keystone/assignment/backends/sql.py b/keystone/assignment/backends/sql.py index eb5b922ef..0bace5e7a 100644 --- a/keystone/assignment/backends/sql.py +++ b/keystone/assignment/backends/sql.py @@ -20,10 +20,12 @@ from keystone import clean from keystone.common import sql from keystone import config from keystone import exception -from keystone.i18n import _ +from keystone.i18n import _, _LE +from keystone.openstack.common import log CONF = config.CONF +LOG = log.getLogger(__name__) class AssignmentType: @@ -311,6 +313,59 @@ class Assignment(keystone_assignment.Driver): query = query.filter(sqlalchemy.or_(*filters)) return [ref.to_dict() for ref in query.all()] + def _get_children(self, session, project_ids): + query = session.query(Project) + query = query.filter(Project.parent_id.in_(project_ids)) + project_refs = query.all() + return [project_ref.to_dict() for project_ref in project_refs] + + def list_projects_in_subtree(self, project_id): + with sql.transaction() as session: + project = self._get_project(session, project_id).to_dict() + children = self._get_children(session, [project['id']]) + subtree = [] + examined = set(project['id']) + while children: + children_ids = set() + for ref in children: + if ref['id'] in examined: + msg = _LE('Circular reference or a repeated ' + 'entry found in projects hierarchy - ' + '%(project_id)s.') + LOG.error(msg, {'project_id': ref['id']}) + return + children_ids.add(ref['id']) + + examined.union(children_ids) + subtree += children + children = self._get_children(session, children_ids) + return subtree + + def list_project_parents(self, project_id): + with sql.transaction() as session: + project = self._get_project(session, project_id).to_dict() + parents = [] + examined = set() + while project.get('parent_id') is not None: + if project['id'] in examined: + msg = _LE('Circular reference or a repeated ' + 'entry found in projects hierarchy - ' + '%(project_id)s.') + LOG.error(msg, {'project_id': project['id']}) + return + + examined.add(project['id']) + parent_project = self._get_project( + session, project['parent_id']).to_dict() + parents.append(parent_project) + project = parent_project + return parents + + def is_leaf_project(self, project_id): + with sql.transaction() as session: + project_refs = self._get_children(session, [project_id]) + return not project_refs + def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None): if project_id is not None: diff --git a/keystone/assignment/controllers.py b/keystone/assignment/controllers.py index bd1261502..66d8e2ea5 100644 --- a/keystone/assignment/controllers.py +++ b/keystone/assignment/controllers.py @@ -402,7 +402,8 @@ class ProjectV3(controller.V3Controller): ref = self.assignment_api.create_project(ref['id'], ref) return ProjectV3.wrap_member(context, ref) - @controller.filterprotected('domain_id', 'enabled', 'name') + @controller.filterprotected('domain_id', 'enabled', 'name', + 'parent_id') def list_projects(self, context, filters): hints = ProjectV3.build_driver_hints(context, filters) refs = self.assignment_api.list_projects(hints=hints) diff --git a/keystone/assignment/core.py b/keystone/assignment/core.py index a16aafcf4..a0f88b73c 100644 --- a/keystone/assignment/core.py +++ b/keystone/assignment/core.py @@ -916,6 +916,45 @@ class Driver(object): raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod + def list_project_parents(self, project_id): + """List all parents from a project by its ID. + + :param project_id: the driver will list the parents of this + project. + + :returns: a list of project_refs or an empty list. + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def list_projects_in_subtree(self, project_id): + """List all projects in the subtree below the hierarchy of the + given project. + + :param project_id: the driver will get the subtree under + this project. + + :returns: a list of project_refs or an empty list + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def is_leaf_project(self, project_id): + """Checks if a project is a leaf in the hierarchy. + + :param project_id: the driver will check if this project + is a leaf in the hierarchy. + + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() + + @abc.abstractmethod def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None): """List all the roles assigned to groups on either domain or project. diff --git a/keystone/tests/test_backend.py b/keystone/tests/test_backend.py index c335203c9..779bd4a9c 100644 --- a/keystone/tests/test_backend.py +++ b/keystone/tests/test_backend.py @@ -2031,6 +2031,122 @@ class IdentityTests(object): self.assertIn(project1['id'], project_ids) self.assertIn(project2['id'], project_ids) + def test_check_leaf_projects(self): + root_project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'parent_id': None} + self.assignment_api.create_project(root_project['id'], root_project) + + leaf_project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'parent_id': root_project['id']} + self.assignment_api.create_project(leaf_project['id'], leaf_project) + + self.assertFalse(self.assignment_api.is_leaf_project( + root_project['id'])) + self.assertTrue(self.assignment_api.is_leaf_project( + leaf_project['id'])) + + # Delete leaf_project + self.assignment_api.delete_project(leaf_project['id']) + + # Now, root_project should be leaf + self.assertTrue(self.assignment_api.is_leaf_project( + root_project['id'])) + + def test_list_projects_in_subtree(self): + project1 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': None} + self.assignment_api.create_project(project1['id'], project1) + + project2 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': project1['id']} + self.assignment_api.create_project(project2['id'], project2) + + project3 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': project2['id']} + self.assignment_api.create_project(project3['id'], project3) + + project4 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': project2['id']} + self.assignment_api.create_project(project4['id'], project4) + + subtree = self.assignment_api.list_projects_in_subtree(project1['id']) + self.assertEqual(3, len(subtree)) + self.assertIn(project2, subtree) + self.assertIn(project3, subtree) + self.assertIn(project4, subtree) + + subtree = self.assignment_api.list_projects_in_subtree(project2['id']) + self.assertEqual(2, len(subtree)) + self.assertIn(project3, subtree) + self.assertIn(project4, subtree) + + subtree = self.assignment_api.list_projects_in_subtree(project3['id']) + self.assertEqual(0, len(subtree)) + + def test_list_project_parents(self): + project1 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': None} + self.assignment_api.create_project(project1['id'], project1) + + project2 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': project1['id']} + self.assignment_api.create_project(project2['id'], project2) + + project3 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': project2['id']} + self.assignment_api.create_project(project3['id'], project3) + + project4 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': project2['id']} + self.assignment_api.create_project(project4['id'], project4) + + parents1 = self.assignment_api.list_project_parents(project3['id']) + self.assertEqual(2, len(parents1)) + self.assertIn(project1, parents1) + self.assertIn(project2, parents1) + + parents2 = self.assignment_api.list_project_parents(project4['id']) + self.assertEqual(parents1, parents2) + + parents = self.assignment_api.list_project_parents(project1['id']) + self.assertEqual(0, len(parents)) + def test_list_roles(self): roles = self.assignment_api.list_roles() self.assertEqual(len(default_fixtures.ROLES), len(roles)) diff --git a/keystone/tests/test_backend_ldap.py b/keystone/tests/test_backend_ldap.py index b9be97b7f..698f32523 100644 --- a/keystone/tests/test_backend_ldap.py +++ b/keystone/tests/test_backend_ldap.py @@ -1546,6 +1546,15 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): self.assignment_api.get_project, project_id) + def test_check_leaf_projects(self): + self.skipTest('N/A: LDAP does not support hierarchical projects') + + def test_list_projects_in_subtree(self): + self.skipTest('N/A: LDAP does not support hierarchical projects') + + def test_list_project_parents(self): + self.skipTest('N/A: LDAP does not support hierarchical projects') + def test_multi_role_grant_by_user_group_on_project_domain(self): # This is a partial implementation of the standard test that # is defined in test_backend.py. It omits both domain and |