Browse Source

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
changes/87/19787/8
Dolph Mathews 9 years ago
committed by Gerrit Code Review
parent
commit
02da3afe4d
  1. 1
      keystone/common/sql/migrate_repo/versions/007_add_domain_tables.py
  2. 3
      keystone/identity/backends/sql.py
  3. 36
      keystone/identity/controllers.py
  4. 34
      keystone/token/controllers.py
  5. 2
      tests/test_sql_upgrade.py
  6. 1
      tests/test_v3.py
  7. 54
      tests/test_v3_identity.py

1
keystone/common/sql/migrate_repo/versions/007_add_domain_tables.py

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

3
keystone/identity/backends/sql.py

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

36
keystone/identity/controllers.py

@ -409,6 +409,35 @@ class DomainV3(controller.V3Controller):
self._require_matching_id(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}
@controller.protected
@ -477,6 +506,13 @@ class UserV3(controller.V3Controller):
self._require_matching_id(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}
@controller.protected

34
keystone/token/controllers.py

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

2
tests/test_sql_upgrade.py

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

1
tests/test_v3.py

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

54
tests/test_v3_identity.py

@ -12,34 +12,26 @@ class IdentityTestCase(test_v3.RestfulTestCase):
self.domain_id = uuid.uuid4().hex
self.domain = self.new_domain_ref()
self.domain['id'] = self.domain_id
self.identity_api.create_domain(
self.domain_id,
self.domain.copy())
self.identity_api.create_domain(self.domain_id, self.domain)
self.project_id = uuid.uuid4().hex
self.project = self.new_project_ref(
domain_id=self.domain_id)
self.project['id'] = self.project_id
self.identity_api.create_project(
self.project_id,
self.project.copy())
self.identity_api.create_project(self.project_id, self.project)
self.user_id = uuid.uuid4().hex
self.user = self.new_user_ref(
domain_id=self.domain_id,
project_id=self.project_id)
self.user['id'] = self.user_id
self.identity_api.create_user(
self.user_id,
self.user.copy())
self.identity_api.create_user(self.user_id, self.user)
self.group_id = uuid.uuid4().hex
self.group = self.new_group_ref(
domain_id=self.domain_id)
self.group['id'] = self.group_id
self.identity_api.create_group(
self.group_id,
self.group.copy())
self.identity_api.create_group(self.group_id, self.group)
self.credential_id = uuid.uuid4().hex
self.credential = self.new_credential_ref(
@ -48,14 +40,12 @@ class IdentityTestCase(test_v3.RestfulTestCase):
self.credential['id'] = self.credential_id
self.identity_api.create_credential(
self.credential_id,
self.credential.copy())
self.credential)
self.role_id = uuid.uuid4().hex
self.role = self.new_role_ref()
self.role['id'] = self.role_id
self.identity_api.create_role(
self.role_id,
self.role.copy())
self.identity_api.create_role(self.role_id, self.role)
# domain validation
@ -202,7 +192,6 @@ class IdentityTestCase(test_v3.RestfulTestCase):
entities = resp.body
self.assertIsNotNone(entities)
self.assertTrue(len(entities))
roles_ref_ids = []
for i, entity in enumerate(entities):
self.assertValidEntity(entity)
self.assertValidGrant(entity, ref)
@ -248,6 +237,27 @@ class IdentityTestCase(test_v3.RestfulTestCase):
body={'domain': 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):
"""DELETE /domains/{domain_id}"""
self.delete('/domains/%(domain_id)s' % {
@ -314,14 +324,14 @@ class IdentityTestCase(test_v3.RestfulTestCase):
def test_add_user_to_group(self):
"""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})
def test_check_user_in_group(self):
"""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})
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})
def test_list_users_in_group(self):
@ -334,9 +344,9 @@ class IdentityTestCase(test_v3.RestfulTestCase):
def test_remove_user_from_group(self):
"""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})
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})
def test_update_user(self):

Loading…
Cancel
Save