Revoke user tokens when disabling/delete a project
- Revoke tokens scoped to all users from a project when disabling or deleting the project. - Fix provided by chmouel Closes-Bug: #1179955 Change-Id: I8ab4713d513b26ced6c37ed026cec9e2df78a5e9
This commit is contained in:
parent
775d7a7802
commit
c70f8c61d5
@ -171,6 +171,12 @@ class V2Controller(wsgi.Application):
|
|||||||
trust['trustee_user_id'],
|
trust['trustee_user_id'],
|
||||||
trust['id'])
|
trust['id'])
|
||||||
|
|
||||||
|
def _delete_tokens_for_project(self, context, project_id):
|
||||||
|
for user_ref in self.identity_api.get_project_users(
|
||||||
|
context, project_id):
|
||||||
|
self._delete_tokens_for_user(
|
||||||
|
context, user_ref['id'], project_id=project_id)
|
||||||
|
|
||||||
def _require_attribute(self, ref, attr):
|
def _require_attribute(self, ref, attr):
|
||||||
"""Ensures the reference contains the specified attribute."""
|
"""Ensures the reference contains the specified attribute."""
|
||||||
if ref.get(attr) is None or ref.get(attr) == '':
|
if ref.get(attr) is None or ref.get(attr) == '':
|
||||||
|
@ -111,12 +111,20 @@ class Tenant(controller.V2Controller):
|
|||||||
# be specifying that
|
# be specifying that
|
||||||
clean_tenant = tenant.copy()
|
clean_tenant = tenant.copy()
|
||||||
clean_tenant.pop('domain_id', None)
|
clean_tenant.pop('domain_id', None)
|
||||||
|
|
||||||
|
# If the project has been disabled (or enabled=False) we are
|
||||||
|
# deleting the tokens for that project.
|
||||||
|
if not tenant.get('enabled', True):
|
||||||
|
self._delete_tokens_for_project(context, tenant_id)
|
||||||
|
|
||||||
tenant_ref = self.identity_api.update_project(
|
tenant_ref = self.identity_api.update_project(
|
||||||
context, tenant_id, clean_tenant)
|
context, tenant_id, clean_tenant)
|
||||||
return {'tenant': tenant_ref}
|
return {'tenant': tenant_ref}
|
||||||
|
|
||||||
def delete_project(self, context, tenant_id):
|
def delete_project(self, context, tenant_id):
|
||||||
self.assert_admin(context)
|
self.assert_admin(context)
|
||||||
|
# Delete all tokens belonging to the users for that project
|
||||||
|
self._delete_tokens_for_project(context, tenant_id)
|
||||||
self.identity_api.delete_project(context, tenant_id)
|
self.identity_api.delete_project(context, tenant_id)
|
||||||
|
|
||||||
def get_project_users(self, context, tenant_id, **kw):
|
def get_project_users(self, context, tenant_id, **kw):
|
||||||
@ -571,6 +579,10 @@ class ProjectV3(controller.V3Controller):
|
|||||||
def update_project(self, context, project_id, project):
|
def update_project(self, context, project_id, project):
|
||||||
self._require_matching_id(project_id, project)
|
self._require_matching_id(project_id, project)
|
||||||
|
|
||||||
|
# The project was disabled so we delete the tokens
|
||||||
|
if not project.get('enabled', True):
|
||||||
|
self._delete_tokens_for_project(context, project_id)
|
||||||
|
|
||||||
ref = self.identity_api.update_project(context, project_id, project)
|
ref = self.identity_api.update_project(context, project_id, project)
|
||||||
return ProjectV3.wrap_member(context, ref)
|
return ProjectV3.wrap_member(context, ref)
|
||||||
|
|
||||||
@ -579,6 +591,10 @@ class ProjectV3(controller.V3Controller):
|
|||||||
for cred in self.identity_api.list_credentials(context):
|
for cred in self.identity_api.list_credentials(context):
|
||||||
if cred['project_id'] == project_id:
|
if cred['project_id'] == project_id:
|
||||||
self.identity_api.delete_credential(context, cred['id'])
|
self.identity_api.delete_credential(context, cred['id'])
|
||||||
|
|
||||||
|
# Delete all tokens belonging to the users for that project
|
||||||
|
self._delete_tokens_for_project(context, project_id)
|
||||||
|
|
||||||
# Finally delete the project itself - the backend is
|
# Finally delete the project itself - the backend is
|
||||||
# responsible for deleting any role assignments related
|
# responsible for deleting any role assignments related
|
||||||
# to this project
|
# to this project
|
||||||
|
@ -379,6 +379,52 @@ class KeystoneClientTests(object):
|
|||||||
client.tokens.authenticate,
|
client.tokens.authenticate,
|
||||||
token=token_id)
|
token=token_id)
|
||||||
|
|
||||||
|
def test_disable_tenant_invalidates_token(self):
|
||||||
|
from keystoneclient import exceptions as client_exceptions
|
||||||
|
|
||||||
|
admin_client = self.get_client(admin=True)
|
||||||
|
foo_client = self.get_client(self.user_foo)
|
||||||
|
tenant_bar = admin_client.tenants.get(self.tenant_bar['id'])
|
||||||
|
|
||||||
|
# Disable the tenant.
|
||||||
|
tenant_bar.update(enabled=False)
|
||||||
|
|
||||||
|
# Test that the token has been removed.
|
||||||
|
self.assertRaises(client_exceptions.Unauthorized,
|
||||||
|
foo_client.tokens.authenticate,
|
||||||
|
token=foo_client.auth_token)
|
||||||
|
|
||||||
|
# Test that the user access has been disabled.
|
||||||
|
self.assertRaises(client_exceptions.Unauthorized,
|
||||||
|
self.get_client,
|
||||||
|
self.user_foo)
|
||||||
|
|
||||||
|
def test_delete_tenant_invalidates_token(self):
|
||||||
|
from keystoneclient import exceptions as client_exceptions
|
||||||
|
|
||||||
|
admin_client = self.get_client(admin=True)
|
||||||
|
foo_client = self.get_client(self.user_foo, self.tenant_bar)
|
||||||
|
tenant_bar = admin_client.tenants.get(self.tenant_bar['id'])
|
||||||
|
|
||||||
|
# Delete the tenant.
|
||||||
|
tenant_bar.delete()
|
||||||
|
|
||||||
|
# Test that the token has been removed.
|
||||||
|
self.assertRaises(client_exceptions.Unauthorized,
|
||||||
|
foo_client.tokens.authenticate,
|
||||||
|
token=foo_client.auth_token)
|
||||||
|
|
||||||
|
# Test that the user access has been disabled.
|
||||||
|
"""
|
||||||
|
# FIXME(dolph): this assertion should not be skipped, but appears to be
|
||||||
|
# an unrelated bug? auth succeeds, even though tenant_bar
|
||||||
|
# was deleted
|
||||||
|
self.assertRaises(client_exceptions.Unauthorized,
|
||||||
|
self.get_client,
|
||||||
|
self.user_foo,
|
||||||
|
self.tenant_bar)
|
||||||
|
"""
|
||||||
|
|
||||||
def test_disable_user_invalidates_token(self):
|
def test_disable_user_invalidates_token(self):
|
||||||
from keystoneclient import exceptions as client_exceptions
|
from keystoneclient import exceptions as client_exceptions
|
||||||
|
|
||||||
@ -1144,6 +1190,12 @@ class KcEssex3TestCase(CompatTestCase, KeystoneClientTests):
|
|||||||
"""Due to lack of endpoint CRUD"""
|
"""Due to lack of endpoint CRUD"""
|
||||||
raise nose.exc.SkipTest('N/A')
|
raise nose.exc.SkipTest('N/A')
|
||||||
|
|
||||||
|
def test_disable_tenant_invalidates_token(self):
|
||||||
|
raise self.skipTest('N/A')
|
||||||
|
|
||||||
|
def test_delete_tenant_invalidates_token(self):
|
||||||
|
raise self.skipTest('N/A')
|
||||||
|
|
||||||
|
|
||||||
class Kc11TestCase(CompatTestCase, KeystoneClientTests):
|
class Kc11TestCase(CompatTestCase, KeystoneClientTests):
|
||||||
def get_checkout(self):
|
def get_checkout(self):
|
||||||
|
@ -595,6 +595,67 @@ class TestTokenRevoking(test_v3.RestfulTestCase):
|
|||||||
headers={'X-Subject-Token': token},
|
headers={'X-Subject-Token': token},
|
||||||
expected_status=204)
|
expected_status=204)
|
||||||
|
|
||||||
|
def test_disabling_project_revokes_token(self):
|
||||||
|
resp = self.post(
|
||||||
|
'/auth/tokens',
|
||||||
|
body=self.build_authentication_request(
|
||||||
|
user_id=self.user3['id'],
|
||||||
|
password=self.user3['password'],
|
||||||
|
project_id=self.projectA['id']))
|
||||||
|
token = resp.getheader('X-Subject-Token')
|
||||||
|
|
||||||
|
# confirm token is valid
|
||||||
|
self.head('/auth/tokens',
|
||||||
|
headers={'X-Subject-Token': token},
|
||||||
|
expected_status=204)
|
||||||
|
|
||||||
|
# disable the project, which should invalidate the token
|
||||||
|
self.patch(
|
||||||
|
'/projects/%(project_id)s' % {'project_id': self.projectA['id']},
|
||||||
|
body={'project': {'enabled': False}})
|
||||||
|
|
||||||
|
# user should no longer have access to the project
|
||||||
|
self.head('/auth/tokens',
|
||||||
|
headers={'X-Subject-Token': token},
|
||||||
|
expected_status=401)
|
||||||
|
resp = self.post(
|
||||||
|
'/auth/tokens',
|
||||||
|
body=self.build_authentication_request(
|
||||||
|
user_id=self.user3['id'],
|
||||||
|
password=self.user3['password'],
|
||||||
|
project_id=self.projectA['id']),
|
||||||
|
expected_status=401)
|
||||||
|
|
||||||
|
def test_deleting_project_revokes_token(self):
|
||||||
|
resp = self.post(
|
||||||
|
'/auth/tokens',
|
||||||
|
body=self.build_authentication_request(
|
||||||
|
user_id=self.user3['id'],
|
||||||
|
password=self.user3['password'],
|
||||||
|
project_id=self.projectA['id']))
|
||||||
|
token = resp.getheader('X-Subject-Token')
|
||||||
|
|
||||||
|
# confirm token is valid
|
||||||
|
self.head('/auth/tokens',
|
||||||
|
headers={'X-Subject-Token': token},
|
||||||
|
expected_status=204)
|
||||||
|
|
||||||
|
# delete the project, which should invalidate the token
|
||||||
|
self.delete(
|
||||||
|
'/projects/%(project_id)s' % {'project_id': self.projectA['id']})
|
||||||
|
|
||||||
|
# user should no longer have access to the project
|
||||||
|
self.head('/auth/tokens',
|
||||||
|
headers={'X-Subject-Token': token},
|
||||||
|
expected_status=401)
|
||||||
|
resp = self.post(
|
||||||
|
'/auth/tokens',
|
||||||
|
body=self.build_authentication_request(
|
||||||
|
user_id=self.user3['id'],
|
||||||
|
password=self.user3['password'],
|
||||||
|
project_id=self.projectA['id']),
|
||||||
|
expected_status=401)
|
||||||
|
|
||||||
def test_deleting_group_grant_revokes_tokens(self):
|
def test_deleting_group_grant_revokes_tokens(self):
|
||||||
"""Test deleting a group grant revokes tokens.
|
"""Test deleting a group grant revokes tokens.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user