Add is_domain field in Project Table

Provides the basic storage of 'is_domain' field in project table.
This flag is not interpreted anywhere yet and is set as False by
default in manager level. We currently do not allow the creation or
seeing a project with this flag set to True via the REST API.
We will allow these operations in subsequent patches.

This attribute is only available in v3. Hence, it's filtered in v2.

Co-Authored-By: Raildo Mascena <raildo@lsd.ufcg.edu.br>
Co-Authored-By: Rodrigo Duarte <rodrigods@lsd.ufcg.edu.br>

Change-Id: I190e1dd04b1474703804de1fb212340fe6626c13
Partially-Implements: bp reseller
This commit is contained in:
Rodrigo Duarte Sousa 2015-04-17 13:55:04 -03:00
parent 0ca655f9c2
commit 0b8248364b
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()