diff --git a/AUTHORS b/AUTHORS index b8d2014c77..861a52539f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -25,6 +25,7 @@ Darren Birkett dcramer Dean Troyer Deepak Garg +Derek Higgins Devin Carlen Dolph Mathews Dolph Mathews diff --git a/keystone/identity/core.py b/keystone/identity/core.py index ee22526455..5efd142e15 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -24,12 +24,15 @@ from keystone import exception from keystone import policy from keystone import token +from keystone.common import logging from keystone.common import manager from keystone.common import wsgi CONF = config.CONF +LOG = logging.getLogger(__name__) + class Manager(manager.Manager): """Default pivot point for the Identity backend. @@ -418,7 +421,16 @@ def set_user_enabled(self, context, user_id, user): return self.update_user(context, user_id, user) def set_user_password(self, context, user_id, user): - return self.update_user(context, user_id, user) + user_ref = self.update_user(context, user_id, user) + try: + for token_id in self.token_api.list_tokens(context, user_id): + self.token_api.delete_token(context, token_id) + except exception.NotImplemented: + # The password has been changed but tokens remain valid for + # backends that can't list tokens for users + LOG.warning('Password changed for %s, but existing tokens remain ' + 'valid' % user_id) + return user_ref def update_user_tenant(self, context, user_id, user): """Update the default tenant.""" diff --git a/keystone/token/backends/kvs.py b/keystone/token/backends/kvs.py index 71b86f8ea7..b033dd31ed 100644 --- a/keystone/token/backends/kvs.py +++ b/keystone/token/backends/kvs.py @@ -44,3 +44,18 @@ def delete_token(self, token_id): return self.db.delete('token-%s' % token_id) except KeyError: raise exception.TokenNotFound(token_id=token_id) + + def list_tokens(self, user_id): + tokens = [] + now = datetime.datetime.utcnow() + for token, user_ref in self.db.items(): + if not token.startswith('token-'): + continue + if 'user' not in user_ref: + continue + if user_ref['user'].get('id') != user_id: + continue + if user_ref.get('expires') and user_ref.get('expires') < now: + continue + tokens.append(token.split('-', 1)[1]) + return tokens diff --git a/keystone/token/backends/sql.py b/keystone/token/backends/sql.py index 7a9a55154a..543f96281f 100644 --- a/keystone/token/backends/sql.py +++ b/keystone/token/backends/sql.py @@ -81,3 +81,17 @@ def delete_token(self, token_id): with session.begin(): session.delete(token_ref) session.flush() + + def list_tokens(self, user_id): + session = self.get_session() + tokens = [] + now = datetime.datetime.utcnow() + for token_ref in session.query(TokenModel)\ + .filter(TokenModel.expires > now): + token_ref_dict = token_ref.to_dict() + if 'user' not in token_ref_dict: + continue + if token_ref_dict['user'].get('id') != user_id: + continue + tokens.append(token_ref['id']) + return tokens diff --git a/keystone/token/core.py b/keystone/token/core.py index 0a28e38208..0d1101dbfd 100644 --- a/keystone/token/core.py +++ b/keystone/token/core.py @@ -87,6 +87,16 @@ def delete_token(self, token_id): """ raise exception.NotImplemented() + def list_tokens(self, user_id): + """Returns a list of current token_id's for a user + + :param user_id: identity of the user + :type user_id: string + :returns: list of token_id's + + """ + raise exception.NotImplemented() + def _get_default_expire_time(self): """Determine when a token should expire based on the config. diff --git a/tests/test_keystoneclient.py b/tests/test_keystoneclient.py index 810d233f10..8d4f002dd2 100644 --- a/tests/test_keystoneclient.py +++ b/tests/test_keystoneclient.py @@ -286,6 +286,29 @@ def test_invalid_user_password(self): username='blah', password='blah') + def test_change_password_invalidates_token(self): + from keystoneclient import exceptions as client_exceptions + + client = self.get_client(admin=True) + + username = uuid.uuid4().hex + passwd = uuid.uuid4().hex + user = client.users.create(name=username, password=passwd, + email=uuid.uuid4().hex) + + token_id = client.tokens.authenticate(username=username, + password=passwd).id + + # authenticate with a token should work before a password change + client.tokens.authenticate(token=token_id) + + client.users.update_password(user=user.id, password=uuid.uuid4().hex) + + # authenticate with a token should not work after a password change + self.assertRaises(client_exceptions.Unauthorized, + client.tokens.authenticate, + token=token_id) + def test_user_create_update_delete(self): from keystoneclient import exceptions as client_exceptions