Enable/disable domains (bug 1100145)

Disabling an individual domain denies auth to users and projects owned by
that domain, and revokes all associated tokens. Re-enabling the domain
does not re-enable tokens.

Change-Id: Ic64f59be4f39317f4c365bec185408e79d18c45f
This commit is contained in:
Dolph Mathews 2013-01-15 23:48:31 -06:00 committed by Gerrit Code Review
parent ac1ed367f6
commit 02da3afe4d
7 changed files with 102 additions and 29 deletions

View File

@ -28,6 +28,7 @@ def upgrade(migrate_engine):
meta, meta,
sql.Column('id', sql.String(64), primary_key=True), sql.Column('id', sql.String(64), primary_key=True),
sql.Column('name', sql.String(64), unique=True, nullable=False), sql.Column('name', sql.String(64), unique=True, nullable=False),
sql.Column('enabled', sql.Boolean, nullable=False, default=True),
sql.Column('extra', sql.Text())) sql.Column('extra', sql.Text()))
domain_table.create(migrate_engine, checkfirst=True) domain_table.create(migrate_engine, checkfirst=True)

View File

@ -72,9 +72,10 @@ class Credential(sql.ModelBase, sql.DictBase):
class Domain(sql.ModelBase, sql.DictBase): class Domain(sql.ModelBase, sql.DictBase):
__tablename__ = 'domain' __tablename__ = 'domain'
attributes = ['id', 'name'] attributes = ['id', 'name', 'enabled']
id = sql.Column(sql.String(64), primary_key=True) id = sql.Column(sql.String(64), primary_key=True)
name = sql.Column(sql.String(64), unique=True, nullable=False) name = sql.Column(sql.String(64), unique=True, nullable=False)
enabled = sql.Column(sql.Boolean, default=True)
extra = sql.Column(sql.JsonBlob()) extra = sql.Column(sql.JsonBlob())

View File

@ -409,6 +409,35 @@ class DomainV3(controller.V3Controller):
self._require_matching_id(domain_id, domain) self._require_matching_id(domain_id, domain)
ref = self.identity_api.update_domain(context, domain_id, domain) ref = self.identity_api.update_domain(context, domain_id, domain)
# disable owned users & projects when the API user specifically set
# enabled=False
# FIXME(dolph): need a driver call to directly revoke all tokens by
# project or domain, regardless of user
if not domain.get('enabled', True):
projects = [x for x in self.identity_api.list_projects(context)
if x.get('domain_id') == domain_id]
for user in self.identity_api.list_users(context):
# TODO(dolph): disable domain-scoped tokens
"""
self.token_api.revoke_tokens(
context,
user_id=user['id'],
domain_id=domain_id)
"""
# revoke all tokens for users owned by this domain
if user.get('domain_id') == domain_id:
self.token_api.revoke_tokens(
context,
user_id=user['id'])
else:
# only revoke tokens on projects owned by this domain
for project in projects:
self.token_api.revoke_tokens(
context,
user_id=user['id'],
tenant_id=project['id'])
return {'domain': ref} return {'domain': ref}
@controller.protected @controller.protected
@ -477,6 +506,13 @@ class UserV3(controller.V3Controller):
self._require_matching_id(user_id, user) self._require_matching_id(user_id, user)
ref = self.identity_api.update_user(context, user_id, user) ref = self.identity_api.update_user(context, user_id, user)
if user.get('password') or not user.get('enabled', True):
# revoke all tokens owned by this user
self.token_api.revoke_tokens(
context,
user_id=user['id'])
return {'user': ref} return {'user': ref}
@controller.protected @controller.protected

View File

@ -83,13 +83,37 @@ class Auth(controller.V2Controller):
LOG.warning(msg) LOG.warning(msg)
raise exception.Unauthorized(msg) raise exception.Unauthorized(msg)
# If the tenant is disabled don't allow them to authenticate # If the user's domain is disabled don't allow them to authenticate
if tenant_ref and not tenant_ref.get('enabled', True): # TODO(dolph): remove this check after default-domain migration
msg = 'Tenant is disabled: %s' % tenant_ref['id'] if user_ref.get('domain_id') is not None:
LOG.warning(msg) user_domain_ref = self.identity_api.get_domain(
raise exception.Unauthorized(msg) context,
user_ref['domain_id'])
if user_domain_ref and not user_domain_ref.get('enabled', True):
msg = 'Domain is disabled: %s' % user_domain_ref['id']
LOG.warning(msg)
raise exception.Unauthorized(msg)
if tenant_ref: if tenant_ref:
# If the project is disabled don't allow them to authenticate
if not tenant_ref.get('enabled', True):
msg = 'Tenant is disabled: %s' % tenant_ref['id']
LOG.warning(msg)
raise exception.Unauthorized(msg)
# If the project's domain is disabled don't allow them to
# authenticate
# TODO(dolph): remove this check after default-domain migration
if tenant_ref.get('domain_id') is not None:
project_domain_ref = self.identity_api.get_domain(
context,
tenant_ref['domain_id'])
if (project_domain_ref and
not project_domain_ref.get('enabled', True)):
msg = 'Domain is disabled: %s' % project_domain_ref['id']
LOG.warning(msg)
raise exception.Unauthorized(msg)
catalog_ref = self.catalog_api.get_catalog( catalog_ref = self.catalog_api.get_catalog(
context=context, context=context,
user_id=user_ref['id'], user_id=user_ref['id'],

View File

@ -346,7 +346,7 @@ class SqlUpgradeTests(test.TestCase):
self.assertTableColumns('credential', ['id', 'user_id', 'project_id', self.assertTableColumns('credential', ['id', 'user_id', 'project_id',
'blob', 'type', 'extra']) 'blob', 'type', 'extra'])
self.assertTableExists('domain') self.assertTableExists('domain')
self.assertTableColumns('domain', ['id', 'name', 'extra']) self.assertTableColumns('domain', ['id', 'name', 'enabled', 'extra'])
self.assertTableExists('user_domain_metadata') self.assertTableExists('user_domain_metadata')
self.assertTableColumns('user_domain_metadata', self.assertTableColumns('user_domain_metadata',
['user_id', 'domain_id', 'data']) ['user_id', 'domain_id', 'data'])

View File

@ -26,6 +26,7 @@ class RestfulTestCase(test_content_types.RestfulTestCase):
self.admin_server.kill() self.admin_server.kill()
self.public_server = None self.public_server = None
self.admin_server = None self.admin_server = None
sql_util.teardown_test_database()
def new_ref(self): def new_ref(self):
"""Populates a ref with attributes common to all API entities.""" """Populates a ref with attributes common to all API entities."""

View File

@ -12,34 +12,26 @@ class IdentityTestCase(test_v3.RestfulTestCase):
self.domain_id = uuid.uuid4().hex self.domain_id = uuid.uuid4().hex
self.domain = self.new_domain_ref() self.domain = self.new_domain_ref()
self.domain['id'] = self.domain_id self.domain['id'] = self.domain_id
self.identity_api.create_domain( self.identity_api.create_domain(self.domain_id, self.domain)
self.domain_id,
self.domain.copy())
self.project_id = uuid.uuid4().hex self.project_id = uuid.uuid4().hex
self.project = self.new_project_ref( self.project = self.new_project_ref(
domain_id=self.domain_id) domain_id=self.domain_id)
self.project['id'] = self.project_id self.project['id'] = self.project_id
self.identity_api.create_project( self.identity_api.create_project(self.project_id, self.project)
self.project_id,
self.project.copy())
self.user_id = uuid.uuid4().hex self.user_id = uuid.uuid4().hex
self.user = self.new_user_ref( self.user = self.new_user_ref(
domain_id=self.domain_id, domain_id=self.domain_id,
project_id=self.project_id) project_id=self.project_id)
self.user['id'] = self.user_id self.user['id'] = self.user_id
self.identity_api.create_user( self.identity_api.create_user(self.user_id, self.user)
self.user_id,
self.user.copy())
self.group_id = uuid.uuid4().hex self.group_id = uuid.uuid4().hex
self.group = self.new_group_ref( self.group = self.new_group_ref(
domain_id=self.domain_id) domain_id=self.domain_id)
self.group['id'] = self.group_id self.group['id'] = self.group_id
self.identity_api.create_group( self.identity_api.create_group(self.group_id, self.group)
self.group_id,
self.group.copy())
self.credential_id = uuid.uuid4().hex self.credential_id = uuid.uuid4().hex
self.credential = self.new_credential_ref( self.credential = self.new_credential_ref(
@ -48,14 +40,12 @@ class IdentityTestCase(test_v3.RestfulTestCase):
self.credential['id'] = self.credential_id self.credential['id'] = self.credential_id
self.identity_api.create_credential( self.identity_api.create_credential(
self.credential_id, self.credential_id,
self.credential.copy()) self.credential)
self.role_id = uuid.uuid4().hex self.role_id = uuid.uuid4().hex
self.role = self.new_role_ref() self.role = self.new_role_ref()
self.role['id'] = self.role_id self.role['id'] = self.role_id
self.identity_api.create_role( self.identity_api.create_role(self.role_id, self.role)
self.role_id,
self.role.copy())
# domain validation # domain validation
@ -202,7 +192,6 @@ class IdentityTestCase(test_v3.RestfulTestCase):
entities = resp.body entities = resp.body
self.assertIsNotNone(entities) self.assertIsNotNone(entities)
self.assertTrue(len(entities)) self.assertTrue(len(entities))
roles_ref_ids = []
for i, entity in enumerate(entities): for i, entity in enumerate(entities):
self.assertValidEntity(entity) self.assertValidEntity(entity)
self.assertValidGrant(entity, ref) self.assertValidGrant(entity, ref)
@ -248,6 +237,27 @@ class IdentityTestCase(test_v3.RestfulTestCase):
body={'domain': ref}) body={'domain': ref})
self.assertValidDomainResponse(r, ref) self.assertValidDomainResponse(r, ref)
def test_disable_domain(self):
"""PATCH /domains/{domain_id} (set enabled=False)"""
self.domain['enabled'] = False
r = self.patch('/domains/%(domain_id)s' % {
'domain_id': self.domain_id},
body={'domain': {'enabled': False}})
self.assertValidDomainResponse(r, self.domain)
# check that the project and user are still enabled
r = self.get('/projects/%(project_id)s' % {
'project_id': self.project_id})
self.assertValidProjectResponse(r, self.project)
self.assertTrue(r.body['project']['enabled'])
r = self.get('/users/%(user_id)s' % {
'user_id': self.user_id})
self.assertValidUserResponse(r, self.user)
self.assertTrue(r.body['user']['enabled'])
# TODO(dolph): assert that v2 & v3 auth return 401
def test_delete_domain(self): def test_delete_domain(self):
"""DELETE /domains/{domain_id}""" """DELETE /domains/{domain_id}"""
self.delete('/domains/%(domain_id)s' % { self.delete('/domains/%(domain_id)s' % {
@ -314,14 +324,14 @@ class IdentityTestCase(test_v3.RestfulTestCase):
def test_add_user_to_group(self): def test_add_user_to_group(self):
"""PUT /groups/{group_id}/users/{user_id}""" """PUT /groups/{group_id}/users/{user_id}"""
r = self.put('/groups/%(group_id)s/users/%(user_id)s' % { self.put('/groups/%(group_id)s/users/%(user_id)s' % {
'group_id': self.group_id, 'user_id': self.user_id}) 'group_id': self.group_id, 'user_id': self.user_id})
def test_check_user_in_group(self): def test_check_user_in_group(self):
"""HEAD /groups/{group_id}/users/{user_id}""" """HEAD /groups/{group_id}/users/{user_id}"""
r = self.put('/groups/%(group_id)s/users/%(user_id)s' % { self.put('/groups/%(group_id)s/users/%(user_id)s' % {
'group_id': self.group_id, 'user_id': self.user_id}) 'group_id': self.group_id, 'user_id': self.user_id})
r = self.head('/groups/%(group_id)s/users/%(user_id)s' % { self.head('/groups/%(group_id)s/users/%(user_id)s' % {
'group_id': self.group_id, 'user_id': self.user_id}) 'group_id': self.group_id, 'user_id': self.user_id})
def test_list_users_in_group(self): def test_list_users_in_group(self):
@ -334,9 +344,9 @@ class IdentityTestCase(test_v3.RestfulTestCase):
def test_remove_user_from_group(self): def test_remove_user_from_group(self):
"""DELETE /groups/{group_id}/users/{user_id}""" """DELETE /groups/{group_id}/users/{user_id}"""
r = self.put('/groups/%(group_id)s/users/%(user_id)s' % { self.put('/groups/%(group_id)s/users/%(user_id)s' % {
'group_id': self.group_id, 'user_id': self.user_id}) 'group_id': self.group_id, 'user_id': self.user_id})
r = self.delete('/groups/%(group_id)s/users/%(user_id)s' % { self.delete('/groups/%(group_id)s/users/%(user_id)s' % {
'group_id': self.group_id, 'user_id': self.user_id}) 'group_id': self.group_id, 'user_id': self.user_id})
def test_update_user(self): def test_update_user(self):