Merge "Add is_domain field in Project Table"

This commit is contained in:
Jenkins 2015-08-20 03:23:08 +00:00 committed by Gerrit Code Review
commit 59ea878c8c
16 changed files with 277 additions and 35 deletions

View File

@ -252,6 +252,12 @@ class V2Controller(wsgi.Application):
ref.pop('parent_id', None)
return ref
@staticmethod
def filter_is_domain(ref):
"""Remove is_domain field since v2 calls are not domain-aware."""
ref.pop('is_domain', None)
return ref
@staticmethod
def normalize_username_in_response(ref):
"""Adds username to outgoing user refs to match the v2 spec.
@ -340,6 +346,7 @@ class V2Controller(wsgi.Application):
"""Run through the various filter methods."""
V2Controller.filter_domain_id(ref)
V2Controller.filter_project_parent_id(ref)
V2Controller.filter_is_domain(ref)
return ref
if isinstance(ref, dict):

View File

@ -130,11 +130,12 @@ class Project(Model):
Optional Keys:
description
enabled (bool, default True)
is_domain (bool, default False)
"""
required_keys = ('id', 'name', 'domain_id')
optional_keys = ('description', 'enabled')
optional_keys = ('description', 'enabled', 'is_domain')
class Role(Model):

View File

@ -0,0 +1,27 @@
# 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
_PROJECT_TABLE_NAME = 'project'
_IS_DOMAIN_COLUMN_NAME = 'is_domain'
def upgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine
project_table = sql.Table(_PROJECT_TABLE_NAME, meta, autoload=True)
is_domain = sql.Column(_IS_DOMAIN_COLUMN_NAME, sql.Boolean, nullable=False,
server_default='0', default=False)
project_table.create_column(is_domain)

View File

@ -60,6 +60,14 @@ class Resource(resource.Driver):
else:
raise ValueError(_('Expected dict or list: %s') % type(ref))
def _set_default_is_domain_project(self, ref):
if isinstance(ref, dict):
return dict(ref, is_domain=False)
elif isinstance(ref, list):
return [self._set_default_is_domain_project(x) for x in ref]
else:
raise ValueError(_('Expected dict or list: %s') % type(ref))
def _validate_parent_project_is_none(self, ref):
"""If a parent_id different from None was given,
raises InvalidProjectException.
@ -69,8 +77,15 @@ class Resource(resource.Driver):
if parent_id is not None:
raise exception.InvalidParentProject(parent_id)
def _validate_is_domain_field_is_false(self, ref):
is_domain = ref.pop('is_domain', None)
if is_domain:
raise exception.ValidationError(_('LDAP does not support projects '
'with is_domain flag enabled'))
def _set_default_attributes(self, project_ref):
project_ref = self._set_default_domain(project_ref)
project_ref = self._set_default_is_domain_project(project_ref)
return self._set_default_parent_project(project_ref)
def get_project(self, tenant_id):
@ -117,6 +132,7 @@ class Resource(resource.Driver):
def create_project(self, tenant_id, tenant):
self.project.check_allow_create()
self._validate_parent_project_is_none(tenant)
self._validate_is_domain_field_is_false(tenant)
tenant['name'] = clean.project_name(tenant['name'])
data = tenant.copy()
if 'id' not in data or data['id'] is None:
@ -129,6 +145,7 @@ class Resource(resource.Driver):
def update_project(self, tenant_id, tenant):
self.project.check_allow_update()
tenant = self._validate_default_domain(tenant)
self._validate_is_domain_field_is_false(tenant)
if 'name' in tenant:
tenant['name'] = clean.project_name(tenant['name'])
return self._set_default_attributes(

View File

@ -245,7 +245,7 @@ class Domain(sql.ModelBase, sql.DictBase):
class Project(sql.ModelBase, sql.DictBase):
__tablename__ = 'project'
attributes = ['id', 'name', 'domain_id', 'description', 'enabled',
'parent_id']
'parent_id', 'is_domain']
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'),
@ -254,6 +254,7 @@ class Project(sql.ModelBase, sql.DictBase):
enabled = sql.Column(sql.Boolean)
extra = sql.Column(sql.JsonBlob())
parent_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'))
is_domain = sql.Column(sql.Boolean, default=False, nullable=False)
# Unique constraint across two columns to create the separation
# rather than just only 'name' being unique
__table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {})

View File

@ -47,24 +47,34 @@ class Tenant(controller.V2Controller):
self.assert_admin(context)
tenant_refs = self.resource_api.list_projects_in_domain(
CONF.identity.default_domain_id)
for tenant_ref in tenant_refs:
tenant_ref = self.v3_to_v2_project(tenant_ref)
tenant_refs = [self.v3_to_v2_project(tenant_ref)
for tenant_ref in tenant_refs
if not tenant_ref.get('is_domain')]
params = {
'limit': context['query_string'].get('limit'),
'marker': context['query_string'].get('marker'),
}
return self.format_project_list(tenant_refs, **params)
def _assert_not_is_domain_project(self, project_id, project_ref=None):
# Projects acting as a domain should not be visible via v2
if not project_ref:
project_ref = self.resource_api.get_project(project_id)
if project_ref.get('is_domain'):
raise exception.ProjectNotFound(project_id)
@controller.v2_deprecated
def get_project(self, context, tenant_id):
# TODO(termie): this stuff should probably be moved to middleware
self.assert_admin(context)
ref = self.resource_api.get_project(tenant_id)
self._assert_not_is_domain_project(tenant_id, ref)
return {'tenant': self.v3_to_v2_project(ref)}
@controller.v2_deprecated
def get_project_by_name(self, context, tenant_name):
self.assert_admin(context)
# Projects acting as a domain should not be visible via v2
ref = self.resource_api.get_project_by_name(
tenant_name, CONF.identity.default_domain_id)
return {'tenant': self.v3_to_v2_project(ref)}
@ -88,11 +98,12 @@ class Tenant(controller.V2Controller):
@controller.v2_deprecated
def update_project(self, context, tenant_id, tenant):
self.assert_admin(context)
# Remove domain_id if specified - a v2 api caller should not
# be specifying that
self._assert_not_is_domain_project(tenant_id)
# Remove domain_id and is_domain if specified - a v2 api caller
# should not be specifying that
clean_tenant = tenant.copy()
clean_tenant.pop('domain_id', None)
clean_tenant.pop('is_domain', None)
tenant_ref = self.resource_api.update_project(
tenant_id, clean_tenant)
return {'tenant': self.v3_to_v2_project(tenant_ref)}
@ -100,6 +111,7 @@ class Tenant(controller.V2Controller):
@controller.v2_deprecated
def delete_project(self, context, tenant_id):
self.assert_admin(context)
self._assert_not_is_domain_project(tenant_id)
self.resource_api.delete_project(tenant_id)
@ -201,6 +213,12 @@ class ProjectV3(controller.V3Controller):
def create_project(self, context, project):
ref = self._assign_unique_id(self._normalize_dict(project))
ref = self._normalize_domain_id(context, ref)
if ref.get('is_domain'):
msg = _('The creation of projects acting as domains is not '
'allowed yet.')
raise exception.NotImplemented(msg)
initiator = notifications._get_request_audit_info(context)
try:
ref = self.resource_api.create_project(ref['id'], ref,

View File

@ -88,6 +88,7 @@ class Manager(manager.Manager):
tenant['enabled'] = clean.project_enabled(tenant['enabled'])
tenant.setdefault('description', '')
tenant.setdefault('parent_id', None)
tenant.setdefault('is_domain', False)
self.get_domain(tenant.get('domain_id'))
if tenant.get('parent_id') is not None:
@ -198,6 +199,11 @@ class Manager(manager.Manager):
raise exception.ForbiddenAction(
action=_('Update of `parent_id` is not allowed.'))
if ('is_domain' in tenant and
tenant['is_domain'] != original_tenant['is_domain']):
raise exception.ValidationError(
message=_('Update of `is_domain` is not allowed.'))
if 'enabled' in tenant:
tenant['enabled'] = clean.project_enabled(tenant['enabled'])

View File

@ -21,6 +21,7 @@ _project_properties = {
# implementation.
'domain_id': parameter_types.id_string,
'enabled': parameter_types.boolean,
'is_domain': parameter_types.boolean,
'parent_id': validation.nullable(parameter_types.id_string),
'name': {
'type': 'string',

View File

@ -25,6 +25,7 @@ TENANTS = [
'description': 'description',
'enabled': True,
'parent_id': None,
'is_domain': False,
}, {
'id': 'baz',
'name': 'BAZ',
@ -32,6 +33,7 @@ TENANTS = [
'description': 'description',
'enabled': True,
'parent_id': None,
'is_domain': False,
}, {
'id': 'mtu',
'name': 'MTU',
@ -39,6 +41,7 @@ TENANTS = [
'enabled': True,
'domain_id': DEFAULT_DOMAIN_ID,
'parent_id': None,
'is_domain': False,
}, {
'id': 'service',
'name': 'service',
@ -46,6 +49,7 @@ TENANTS = [
'enabled': True,
'domain_id': DEFAULT_DOMAIN_ID,
'parent_id': None,
'is_domain': False,
}
]

View File

@ -2089,7 +2089,7 @@ class IdentityTests(object):
# Create a project
project = {'id': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID,
'name': uuid.uuid4().hex, 'description': uuid.uuid4().hex,
'enabled': True, 'parent_id': None}
'enabled': True, 'parent_id': None, 'is_domain': False}
self.resource_api.create_project(project['id'], project)
# Build driver hints with the project's name and inexistent description
@ -2157,7 +2157,8 @@ class IdentityTests(object):
'domain_id': domain_id,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(project_id, project)
projects = [project]
@ -2167,13 +2168,38 @@ class IdentityTests(object):
'domain_id': domain_id,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': project_id}
'parent_id': project_id,
'is_domain': False}
self.resource_api.create_project(new_project['id'], new_project)
projects.append(new_project)
project_id = new_project['id']
return projects
def test_create_project_without_is_domain_flag(self):
project = {'id': uuid.uuid4().hex,
'description': '',
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': None}
ref = self.resource_api.create_project(project['id'], project)
# The is_domain flag should be False by default
self.assertFalse(ref['is_domain'])
def test_create_is_domain_project(self):
project = {'id': uuid.uuid4().hex,
'description': '',
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': None,
'is_domain': True}
ref = self.resource_api.create_project(project['id'], project)
self.assertTrue(ref['is_domain'])
def test_check_leaf_projects(self):
projects_hierarchy = self._create_projects_hierarchy()
root_project = projects_hierarchy[0]
@ -2201,7 +2227,8 @@ class IdentityTests(object):
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': project2['id']}
'parent_id': project2['id'],
'is_domain': False}
self.resource_api.create_project(project4['id'], project4)
subtree = self.resource_api.list_projects_in_subtree(project1['id'])
@ -2270,7 +2297,8 @@ class IdentityTests(object):
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': project2['id']}
'parent_id': project2['id'],
'is_domain': False}
self.resource_api.create_project(project4['id'], project4)
parents1 = self.resource_api.list_project_parents(project3['id'])
@ -2873,7 +2901,8 @@ class IdentityTests(object):
'description': '',
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'parent_id': 'fake'}
'parent_id': 'fake',
'is_domain': False}
self.assertRaises(exception.ProjectNotFound,
self.resource_api.create_project,
project['id'],
@ -2886,7 +2915,8 @@ class IdentityTests(object):
'description': '',
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(root_project['id'], root_project)
domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
@ -2897,7 +2927,8 @@ class IdentityTests(object):
'description': '',
'domain_id': domain['id'],
'enabled': True,
'parent_id': root_project['id']}
'parent_id': root_project['id'],
'is_domain': False}
self.assertRaises(exception.ValidationError,
self.resource_api.create_project,
@ -2948,13 +2979,15 @@ class IdentityTests(object):
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': False,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(project1['id'], project1)
project2 = {'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'parent_id': project1['id']}
'parent_id': project1['id'],
'is_domain': False}
# It's not possible to create a project under a disabled one in the
# hierarchy
@ -3020,7 +3053,8 @@ class IdentityTests(object):
'id': project_id,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'parent_id': leaf_project['id']}
'parent_id': leaf_project['id'],
'is_domain': False}
self.assertRaises(exception.ForbiddenAction,
self.resource_api.create_project,
project_id,
@ -3032,7 +3066,8 @@ class IdentityTests(object):
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(project['id'], project)
# Add a description attribute.
@ -3048,7 +3083,8 @@ class IdentityTests(object):
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(project['id'], project)
# Add a description attribute.
@ -3726,16 +3762,16 @@ class IdentityTests(object):
domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.resource_api.create_domain(domain2['id'], domain2)
project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain1['id']}
'domain_id': domain1['id'], 'is_domain': False}
project1 = self.resource_api.create_project(project1['id'], project1)
project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain1['id']}
'domain_id': domain1['id'], 'is_domain': False}
project2 = self.resource_api.create_project(project2['id'], project2)
project3 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain1['id']}
'domain_id': domain1['id'], 'is_domain': False}
project3 = self.resource_api.create_project(project3['id'], project3)
project4 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain2['id']}
'domain_id': domain2['id'], 'is_domain': False}
project4 = self.resource_api.create_project(project4['id'], project4)
group_list = []
role_list = []
@ -5538,14 +5574,16 @@ class InheritanceTests(object):
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(root_project['id'], root_project)
leaf_project = {'id': uuid.uuid4().hex,
'description': '',
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': root_project['id']}
'parent_id': root_project['id'],
'is_domain': False}
self.resource_api.create_project(leaf_project['id'], leaf_project)
user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex,
@ -5659,14 +5697,16 @@ class InheritanceTests(object):
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(root_project['id'], root_project)
leaf_project = {'id': uuid.uuid4().hex,
'description': '',
'domain_id': DEFAULT_DOMAIN_ID,
'enabled': True,
'name': uuid.uuid4().hex,
'parent_id': root_project['id']}
'parent_id': root_project['id'],
'is_domain': False}
self.resource_api.create_project(leaf_project['id'], leaf_project)
user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex,

View File

@ -1532,7 +1532,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
'domain_id': CONF.identity.default_domain_id,
'description': uuid.uuid4().hex,
'enabled': True,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(project['id'], project)
project_ref = self.resource_api.get_project(project['id'])
@ -1610,7 +1611,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
'description': '',
'domain_id': domain['id'],
'enabled': True,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(project1['id'], project1)
# Creating project2 under project1. LDAP will not allow
@ -1620,7 +1622,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
'description': '',
'domain_id': domain['id'],
'enabled': True,
'parent_id': project1['id']}
'parent_id': project1['id'],
'is_domain': False}
self.assertRaises(exception.InvalidParentProject,
self.resource_api.create_project,
@ -1634,6 +1637,37 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
# Returning projects to be used across the tests
return [project1, project2]
def test_create_is_domain_project(self):
domain = self._get_domain_fixture()
project = {'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'description': '',
'domain_id': domain['id'],
'enabled': True,
'parent_id': None,
'is_domain': True}
self.assertRaises(exception.ValidationError,
self.resource_api.create_project,
project['id'], project)
def test_update_is_domain_field(self):
domain = self._get_domain_fixture()
project = {'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'description': '',
'domain_id': domain['id'],
'enabled': True,
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(project['id'], project)
# Try to update the is_domain field to True
project['is_domain'] = True
self.assertRaises(exception.ValidationError,
self.resource_api.update_project,
project['id'], project)
def test_check_leaf_projects(self):
projects = self._assert_create_hierarchy_not_allowed()
for project in projects:
@ -1966,7 +2000,8 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity):
'name': uuid.uuid4().hex,
'domain_id': CONF.identity.default_domain_id,
'description': uuid.uuid4().hex,
'parent_id': None}
'parent_id': None,
'is_domain': False}
self.resource_api.create_project(project['id'], project)
project_ref = self.resource_api.get_project(project['id'])
@ -2603,7 +2638,8 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, tests.SQLDriverOverrides,
'domain_id': domain['id'],
'description': uuid.uuid4().hex,
'parent_id': None,
'enabled': True}
'enabled': True,
'is_domain': False}
self.resource_api.create_domain(domain['id'], domain)
self.resource_api.create_project(project['id'], project)
project_ref = self.resource_api.get_project(project['id'])

View File

@ -156,7 +156,8 @@ class SqlModels(SqlTests):
('domain_id', sql.String, 64),
('enabled', sql.Boolean, None),
('extra', sql.JsonBlob, None),
('parent_id', sql.String, 64))
('parent_id', sql.String, 64),
('is_domain', sql.Boolean, False))
self.assertExpectedSchema('project', cols)
def test_role_assignment_model(self):

View File

@ -629,6 +629,13 @@ class SqlUpgradeTests(SqlMigrateBase):
self.assertFalse(self._does_index_exist('assignment',
'assignment_role_id_fkey'))
def test_project_is_domain_upgrade(self):
self.upgrade(74)
self.assertTableColumns('project',
['id', 'name', 'extra', 'description',
'enabled', 'domain_id', 'parent_id',
'is_domain'])
def populate_user_table(self, with_pass_enab=False,
with_pass_enab_domain=False):
# Populate the appropriate fields in the user

View File

@ -16,6 +16,7 @@
import uuid
from keystone.assignment import controllers as assignment_controllers
from keystone import exception
from keystone.resource import controllers as resource_controllers
from keystone.tests import unit as tests
from keystone.tests.unit import default_fixtures
@ -93,4 +94,50 @@ class TenantTestCase(tests.TestCase):
tenant_copy = tenant.copy()
tenant_copy.pop('domain_id')
tenant_copy.pop('parent_id')
tenant_copy.pop('is_domain')
self.assertIn(tenant_copy, refs['tenants'])
def _create_is_domain_project(self):
project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': 'default', 'is_domain': True}
project_ref = self.resource_api.create_project(project['id'], project)
return self.tenant_controller.v3_to_v2_project(project_ref)
def test_update_is_domain_project_not_found(self):
"""Test that update is_domain project is not allowed in v2."""
project = self._create_is_domain_project()
project['name'] = uuid.uuid4().hex
self.assertRaises(
exception.ProjectNotFound,
self.tenant_controller.update_project,
_ADMIN_CONTEXT,
project['id'],
project
)
def test_delete_is_domain_project_not_found(self):
"""Test that delete is_domain project is not allowed in v2."""
project = self._create_is_domain_project()
self.assertRaises(
exception.ProjectNotFound,
self.tenant_controller.delete_project,
_ADMIN_CONTEXT,
project['id']
)
def test_list_is_domain_project_not_found(self):
"""Test v2 get_all_projects having projects that act as a domain.
In v2 no project with the is_domain flag enabled should be
returned.
"""
project1 = self._create_is_domain_project()
project2 = self._create_is_domain_project()
refs = self.tenant_controller.get_all_projects(_ADMIN_CONTEXT)
projects = refs.get('tenants')
self.assertNotIn(project1, projects)
self.assertNotIn(project2, projects)

View File

@ -299,10 +299,11 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase,
ref = self.new_ref()
return ref
def new_project_ref(self, domain_id, parent_id=None):
def new_project_ref(self, domain_id=None, parent_id=None, is_domain=False):
ref = self.new_ref()
ref['domain_id'] = domain_id
ref['parent_id'] = parent_id
ref['is_domain'] = is_domain
return ref
def new_user_ref(self, domain_id, project_id=None):

View File

@ -527,6 +527,18 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
ref = self.new_project_ref(domain_id=uuid.uuid4().hex)
self.post('/projects', body={'project': ref}, expected_status=400)
def test_create_project_is_domain_not_allowed(self):
"""Call ``POST /projects``.
Setting is_domain=True is not supported yet and should raise
NotImplemented.
"""
ref = self.new_project_ref(domain_id=self.domain_id, is_domain=True)
self.post('/projects',
body={'project': ref},
expected_status=501)
def _create_projects_hierarchy(self, hierarchy_size=1):
"""Creates a single-branched project hierarchy with the specified size.
@ -942,6 +954,22 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
body={'project': leaf_project},
expected_status=403)
def test_update_project_is_domain_not_allowed(self):
"""Call ``PATCH /projects/{project_id}`` with is_domain.
The is_domain flag is immutable.
"""
project = self.new_project_ref(domain_id=self.domain['id'])
resp = self.post('/projects',
body={'project': project})
self.assertFalse(resp.result['project']['is_domain'])
project['is_domain'] = True
self.patch('/projects/%(project_id)s' % {
'project_id': resp.result['project']['id']},
body={'project': project},
expected_status=400)
def test_disable_leaf_project(self):
"""Call ``PATCH /projects/{project_id}``."""
projects = self._create_projects_hierarchy()