diff --git a/keystone/common/sql/migrate_repo/versions/007_add_domain_tables.py b/keystone/common/sql/migrate_repo/versions/007_add_domain_tables.py index 91970506d0..17f8779934 100644 --- a/keystone/common/sql/migrate_repo/versions/007_add_domain_tables.py +++ b/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) diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py index da2c5a3dc2..0d477fd0f6 100644 --- a/keystone/identity/backends/sql.py +++ b/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()) diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 35c1cccf4b..0ae54202bc 100644 --- a/keystone/identity/controllers.py +++ b/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 diff --git a/keystone/token/controllers.py b/keystone/token/controllers.py index 7035986939..a20156952c 100644 --- a/keystone/token/controllers.py +++ b/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'], diff --git a/tests/test_sql_upgrade.py b/tests/test_sql_upgrade.py index 998e975370..4acda07b4d 100644 --- a/tests/test_sql_upgrade.py +++ b/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']) diff --git a/tests/test_v3.py b/tests/test_v3.py index 4c7615d7df..958260dda5 100644 --- a/tests/test_v3.py +++ b/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.""" diff --git a/tests/test_v3_identity.py b/tests/test_v3_identity.py index 0409853777..5236cddcfc 100644 --- a/tests/test_v3_identity.py +++ b/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):