summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRodrigo Duarte Sousa <rodrigods@lsd.ufcg.edu.br>2014-08-04 17:36:58 -0300
committerRodrigo Duarte Sousa <rodrigods@lsd.ufcg.edu.br>2014-10-21 08:59:06 -0300
commita98858e17087a6a1f3862d1a38c66f164fa15279 (patch)
tree191ffe9949a34a05f6e9d5b838e9617d5982d975
parentd81f23a6c3abf882c20b7c3e86a6c5315d9fb834 (diff)
downloadkeystone-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.py15
-rw-r--r--keystone/assignment/backends/sql.py57
-rw-r--r--keystone/assignment/controllers.py3
-rw-r--r--keystone/assignment/core.py39
-rw-r--r--keystone/tests/test_backend.py116
-rw-r--r--keystone/tests/test_backend_ldap.py9
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