Add parent_id field to projects
Field to point to a parent project. This enables the possibility to use hierarchical 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> Implements: blueprint hierarchical-multitenancy Change-Id: I2b56a362df40b664e3a802d09ca027277678b300
This commit is contained in:
@@ -51,11 +51,28 @@ class Assignment(assignment.Driver):
|
||||
self.project = ProjectApi(CONF)
|
||||
self.role = RoleApi(CONF, self.user)
|
||||
|
||||
def _set_default_parent_project(self, ref):
|
||||
"""If the parent project ID has not been set, set it to None."""
|
||||
if isinstance(ref, dict):
|
||||
if 'parent_id' not in ref:
|
||||
ref = dict(ref, parent_id=None)
|
||||
return ref
|
||||
elif isinstance(ref, list):
|
||||
return [self._set_default_parent_project(x) for x in ref]
|
||||
else:
|
||||
raise ValueError(_('Expected dict or list: %s') % type(ref))
|
||||
|
||||
def _set_default_attributes(self, project_ref):
|
||||
project_ref = self._set_default_domain(project_ref)
|
||||
return self._set_default_parent_project(project_ref)
|
||||
|
||||
def get_project(self, tenant_id):
|
||||
return self._set_default_domain(self.project.get(tenant_id))
|
||||
return self._set_default_attributes(
|
||||
self.project.get(tenant_id))
|
||||
|
||||
def list_projects(self, hints):
|
||||
return self._set_default_domain(self.project.get_all())
|
||||
return self._set_default_attributes(
|
||||
self.project.get_all())
|
||||
|
||||
def list_projects_in_domain(self, domain_id):
|
||||
# We don't support multiple domains within this driver, so ignore
|
||||
@@ -64,7 +81,8 @@ class Assignment(assignment.Driver):
|
||||
|
||||
def get_project_by_name(self, tenant_name, domain_id):
|
||||
self._validate_default_domain_id(domain_id)
|
||||
return self._set_default_domain(self.project.get_by_name(tenant_name))
|
||||
return self._set_default_attributes(
|
||||
self.project.get_by_name(tenant_name))
|
||||
|
||||
def create_project(self, tenant_id, tenant):
|
||||
self.project.check_allow_create()
|
||||
@@ -75,14 +93,16 @@ class Assignment(assignment.Driver):
|
||||
data['id'] = str(uuid.uuid4().hex)
|
||||
if 'description' in data and data['description'] in ['', None]:
|
||||
data.pop('description')
|
||||
return self._set_default_domain(self.project.create(data))
|
||||
return self._set_default_attributes(
|
||||
self.project.create(data))
|
||||
|
||||
def update_project(self, tenant_id, tenant):
|
||||
self.project.check_allow_update()
|
||||
tenant = self._validate_default_domain(tenant)
|
||||
if 'name' in tenant:
|
||||
tenant['name'] = clean.project_name(tenant['name'])
|
||||
return self._set_default_domain(self.project.update(tenant_id, tenant))
|
||||
return self._set_default_attributes(
|
||||
self.project.update(tenant_id, tenant))
|
||||
|
||||
def get_group_project_roles(self, groups, project_id, project_domain_id):
|
||||
self.get_project(project_id)
|
||||
@@ -153,7 +173,7 @@ class Assignment(assignment.Driver):
|
||||
# Since the LDAP backend doesn't store the domain_id in the LDAP
|
||||
# records (and only supports the default domain), we fill in the
|
||||
# domain_id before we return the list.
|
||||
return [self._set_default_domain(x) for x in
|
||||
return [self._set_default_attributes(x) for x in
|
||||
self.project.get_user_projects(user_dn, associations)]
|
||||
|
||||
def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None):
|
||||
|
||||
@@ -607,7 +607,8 @@ class Domain(sql.ModelBase, sql.DictBase):
|
||||
|
||||
class Project(sql.ModelBase, sql.DictBase):
|
||||
__tablename__ = 'project'
|
||||
attributes = ['id', 'name', 'domain_id', 'description', 'enabled']
|
||||
attributes = ['id', 'name', 'domain_id', 'description', 'enabled',
|
||||
'parent_id']
|
||||
id = sql.Column(sql.String(64), primary_key=True)
|
||||
name = sql.Column(sql.String(64), nullable=False)
|
||||
domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'),
|
||||
@@ -615,6 +616,7 @@ class Project(sql.ModelBase, sql.DictBase):
|
||||
description = sql.Column(sql.Text())
|
||||
enabled = sql.Column(sql.Boolean)
|
||||
extra = sql.Column(sql.JsonBlob())
|
||||
parent_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'))
|
||||
# Unique constraint across two columns to create the separation
|
||||
# rather than just only 'name' being unique
|
||||
__table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {})
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# 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.
|
||||
|
||||
import sqlalchemy as sql
|
||||
|
||||
from keystone.common.sql import migration_helpers
|
||||
|
||||
_PROJECT_TABLE_NAME = 'project'
|
||||
_PARENT_ID_COLUMN_NAME = 'parent_id'
|
||||
|
||||
|
||||
def list_constraints(project_table):
|
||||
constraints = [{'table': project_table,
|
||||
'fk_column': _PARENT_ID_COLUMN_NAME,
|
||||
'ref_column': project_table.c.id}]
|
||||
|
||||
return constraints
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = sql.MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
project_table = sql.Table(_PROJECT_TABLE_NAME, meta, autoload=True)
|
||||
parent_id = sql.Column(_PARENT_ID_COLUMN_NAME, sql.String(64),
|
||||
nullable=True)
|
||||
project_table.create_column(parent_id)
|
||||
|
||||
if migrate_engine.name == 'sqlite':
|
||||
return
|
||||
migration_helpers.add_constraints(list_constraints(project_table))
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = sql.MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
# SQLite does not support constraints, and querying the constraints
|
||||
# raises an exception
|
||||
if migrate_engine.name != 'sqlite':
|
||||
migration_helpers.remove_constraints(list_constraints(meta))
|
||||
|
||||
project_table = sql.Table(_PROJECT_TABLE_NAME, meta, autoload=True)
|
||||
project_table.drop_column(_PARENT_ID_COLUMN_NAME)
|
||||
@@ -24,24 +24,28 @@ TENANTS = [
|
||||
'domain_id': DEFAULT_DOMAIN_ID,
|
||||
'description': 'description',
|
||||
'enabled': True,
|
||||
'parent_id': None,
|
||||
}, {
|
||||
'id': 'baz',
|
||||
'name': 'BAZ',
|
||||
'domain_id': DEFAULT_DOMAIN_ID,
|
||||
'description': 'description',
|
||||
'enabled': True,
|
||||
'parent_id': None,
|
||||
}, {
|
||||
'id': 'mtu',
|
||||
'name': 'MTU',
|
||||
'description': 'description',
|
||||
'enabled': True,
|
||||
'domain_id': DEFAULT_DOMAIN_ID
|
||||
'domain_id': DEFAULT_DOMAIN_ID,
|
||||
'parent_id': None,
|
||||
}, {
|
||||
'id': 'service',
|
||||
'name': 'service',
|
||||
'description': 'description',
|
||||
'enabled': True,
|
||||
'domain_id': DEFAULT_DOMAIN_ID
|
||||
'domain_id': DEFAULT_DOMAIN_ID,
|
||||
'parent_id': None,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -2507,7 +2507,8 @@ class IdentityTests(object):
|
||||
project = {'id': uuid.uuid4().hex,
|
||||
'name': uuid.uuid4().hex,
|
||||
'domain_id': DEFAULT_DOMAIN_ID,
|
||||
'enabled': True}
|
||||
'enabled': True,
|
||||
'parent_id': None}
|
||||
self.assignment_api.create_project(project['id'], project)
|
||||
|
||||
# Add a description attribute.
|
||||
@@ -2522,7 +2523,8 @@ class IdentityTests(object):
|
||||
project = {'id': uuid.uuid4().hex,
|
||||
'name': uuid.uuid4().hex,
|
||||
'domain_id': DEFAULT_DOMAIN_ID,
|
||||
'enabled': True}
|
||||
'enabled': True,
|
||||
'parent_id': None}
|
||||
self.assignment_api.create_project(project['id'], project)
|
||||
|
||||
# Add a description attribute.
|
||||
|
||||
@@ -1471,10 +1471,12 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
|
||||
# NOTE(topol): LDAP implementation does not currently support the
|
||||
# updating of a project name so this method override
|
||||
# provides a different update test
|
||||
project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
|
||||
project = {'id': uuid.uuid4().hex,
|
||||
'name': uuid.uuid4().hex,
|
||||
'domain_id': CONF.identity.default_domain_id,
|
||||
'description': uuid.uuid4().hex, 'enabled': True
|
||||
}
|
||||
'description': uuid.uuid4().hex,
|
||||
'enabled': True,
|
||||
'parent_id': None}
|
||||
self.assignment_api.create_project(project['id'], project)
|
||||
project_ref = self.assignment_api.get_project(project['id'])
|
||||
|
||||
@@ -1812,7 +1814,8 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity):
|
||||
'id': uuid.uuid4().hex,
|
||||
'name': uuid.uuid4().hex,
|
||||
'domain_id': CONF.identity.default_domain_id,
|
||||
'description': uuid.uuid4().hex}
|
||||
'description': uuid.uuid4().hex,
|
||||
'parent_id': None}
|
||||
|
||||
self.assignment_api.create_project(project['id'], project)
|
||||
project_ref = self.assignment_api.get_project(project['id'])
|
||||
@@ -2426,10 +2429,12 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, tests.SQLDriverOverrides,
|
||||
def test_delete_domain_with_user_added(self):
|
||||
domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
|
||||
'enabled': True}
|
||||
project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
|
||||
project = {'id': uuid.uuid4().hex,
|
||||
'name': uuid.uuid4().hex,
|
||||
'domain_id': domain['id'],
|
||||
'description': uuid.uuid4().hex, 'enabled': True
|
||||
}
|
||||
'description': uuid.uuid4().hex,
|
||||
'parent_id': None,
|
||||
'enabled': True}
|
||||
self.assignment_api.create_domain(domain['id'], domain)
|
||||
self.assignment_api.create_project(project['id'], project)
|
||||
project_ref = self.assignment_api.get_project(project['id'])
|
||||
|
||||
@@ -1462,6 +1462,50 @@ class SqlUpgradeTests(SqlMigrateBase):
|
||||
index_data = [(idx.name, idx.columns.keys()) for idx in table.indexes]
|
||||
self.assertNotIn(('ix_actor_id', ['actor_id']), index_data)
|
||||
|
||||
def test_project_parent_id_upgrade(self):
|
||||
self.upgrade(61)
|
||||
self.assertTableColumns('project',
|
||||
['id', 'name', 'extra', 'description',
|
||||
'enabled', 'domain_id', 'parent_id'])
|
||||
|
||||
def test_project_parent_id_downgrade(self):
|
||||
self.upgrade(61)
|
||||
self.downgrade(60)
|
||||
self.assertTableColumns('project',
|
||||
['id', 'name', 'extra', 'description',
|
||||
'enabled', 'domain_id'])
|
||||
|
||||
def test_project_parent_id_cleanup(self):
|
||||
# make sure that the parent_id field is dropped in the downgrade
|
||||
self.upgrade(61)
|
||||
session = self.Session()
|
||||
beta = {
|
||||
'id': uuid.uuid4().hex,
|
||||
'description': uuid.uuid4().hex,
|
||||
'domain_id': uuid.uuid4().hex,
|
||||
'name': uuid.uuid4().hex,
|
||||
'parent_id': uuid.uuid4().hex
|
||||
}
|
||||
acme = {
|
||||
'id': uuid.uuid4().hex,
|
||||
'description': uuid.uuid4().hex,
|
||||
'domain_id': uuid.uuid4().hex,
|
||||
'name': uuid.uuid4().hex,
|
||||
'parent_id': None
|
||||
}
|
||||
self.insert_dict(session, 'project', beta)
|
||||
self.insert_dict(session, 'project', acme)
|
||||
proj_table = sqlalchemy.Table('project', self.metadata, autoload=True)
|
||||
self.assertEqual(2, session.query(proj_table).count())
|
||||
session.close()
|
||||
self.downgrade(60)
|
||||
session = self.Session()
|
||||
self.metadata.clear()
|
||||
proj_table = sqlalchemy.Table('project', self.metadata, autoload=True)
|
||||
self.assertEqual(2, session.query(proj_table).count())
|
||||
project = session.query(proj_table)[0]
|
||||
self.assertRaises(AttributeError, getattr, project, 'parent_id')
|
||||
|
||||
def populate_user_table(self, with_pass_enab=False,
|
||||
with_pass_enab_domain=False):
|
||||
# Populate the appropriate fields in the user
|
||||
|
||||
@@ -298,9 +298,10 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase,
|
||||
ref = self.new_ref()
|
||||
return ref
|
||||
|
||||
def new_project_ref(self, domain_id):
|
||||
def new_project_ref(self, domain_id, parent_id=None):
|
||||
ref = self.new_ref()
|
||||
ref['domain_id'] = domain_id
|
||||
ref['parent_id'] = parent_id
|
||||
return ref
|
||||
|
||||
def new_user_ref(self, domain_id, project_id=None):
|
||||
|
||||
Reference in New Issue
Block a user