diff options
-rw-r--r-- | etc/policy.json | 5 | ||||
-rw-r--r-- | keystone/auth/token_factory.py | 3 | ||||
-rw-r--r-- | keystone/common/controller.py | 31 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/021_add_trust_to_token.py | 82 | ||||
-rw-r--r-- | keystone/identity/controllers.py | 78 | ||||
-rw-r--r-- | keystone/token/backends/kvs.py | 4 | ||||
-rw-r--r-- | keystone/token/backends/memcache.py | 2 | ||||
-rw-r--r-- | keystone/token/backends/sql.py | 27 | ||||
-rw-r--r-- | keystone/token/core.py | 17 | ||||
-rw-r--r-- | tests/test_auth.py | 4 | ||||
-rw-r--r-- | tests/test_backend.py | 6 | ||||
-rw-r--r-- | tests/test_sql_upgrade.py | 8 | ||||
-rw-r--r-- | tests/test_v3.py | 7 | ||||
-rw-r--r-- | tests/test_v3_auth.py | 48 |
14 files changed, 214 insertions, 108 deletions
diff --git a/etc/policy.json b/etc/policy.json index 89365e5e..17da8eac 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -25,8 +25,7 @@ "identity:get_project": [["rule:admin_required"]], "identity:list_projects": [["rule:admin_required"]], - "identity:list_user_projects": [["rule:admin_required"], - ["user_id:%(user_id)s"]], + "identity:list_user_projects": [["rule:admin_or_owner"]], "identity:create_project": [["rule:admin_or_owner"]], "identity:update_project": [["rule:admin_required"]], "identity:delete_project": [["rule:admin_required"]], @@ -34,7 +33,7 @@ "identity:get_user": [["rule:admin_required"]], "identity:list_users": [["rule:admin_required"]], "identity:create_user": [["rule:admin_required"]], - "identity:update_user": [["rule:admin_required"]], + "identity:update_user": [["rule:admin_or_owner"]], "identity:delete_user": [["rule:admin_required"]], "identity:get_group": [["rule:admin_required"]], diff --git a/keystone/auth/token_factory.py b/keystone/auth/token_factory.py index 172216e9..0ec2fe28 100644 --- a/keystone/auth/token_factory.py +++ b/keystone/auth/token_factory.py @@ -285,7 +285,8 @@ def create_token(context, auth_context, auth_info): user=token_data['token']['user'], tenant=token_data['token'].get('project'), metadata=metadata_ref, - token_data=token_data) + token_data=token_data, + trust_id=trust['id'] if trust else None) token_api.create_token(context, token_id, data) except Exception as e: # an identical token may have been created already. diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 09da9d7b..c7425ae8 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -153,6 +153,32 @@ def filterprotected(*filters): class V2Controller(wsgi.Application): """Base controller class for Identity API v2.""" + def _delete_tokens_for_trust(self, context, user_id, trust_id): + try: + token_list = self.token_api.list_tokens(context, user_id, + trust_id=trust_id) + for token in token_list: + self.token_api.delete_token(context, token) + except exception.NotFound: + pass + + def _delete_tokens_for_user(self, context, user_id, project_id=None): + #First delete tokens that could get other tokens. + for token_id in self.token_api.list_tokens(context, + user_id, + tenant_id=project_id): + try: + self.token_api.delete_token(context, token_id) + except exception.NotFound: + pass + #delete tokens generated from trusts + for trust in self.trust_api.list_trusts_for_trustee(context, user_id): + self._delete_tokens_for_trust(context, user_id, trust['id']) + for trust in self.trust_api.list_trusts_for_trustor(context, user_id): + self._delete_tokens_for_trust(context, + trust['trustee_user_id'], + trust['id']) + def _require_attribute(self, ref, attr): """Ensures the reference contains the specified attribute.""" if ref.get(attr) is None or ref.get(attr) == '': @@ -188,6 +214,11 @@ class V3Controller(V2Controller): collection_name = 'entities' member_name = 'entity' + def _delete_tokens_for_group(self, context, group_id): + user_refs = self.identity_api.list_users_in_group(context, group_id) + for user in user_refs: + self._delete_tokens_for_user(context, user['id']) + @classmethod def base_url(cls, path=None): endpoint = CONF.public_endpoint % CONF diff --git a/keystone/common/sql/migrate_repo/versions/021_add_trust_to_token.py b/keystone/common/sql/migrate_repo/versions/021_add_trust_to_token.py new file mode 100644 index 00000000..caad8674 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/021_add_trust_to_token.py @@ -0,0 +1,82 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import sqlalchemy +from sqlalchemy import exc +from sqlalchemy.orm import sessionmaker + +from keystone import config + + +def downgrade_token_table_with_column_drop(meta, migrate_engine): + token_table = sqlalchemy.Table('token', meta, autoload=True) + #delete old tokens, as the format has changed. + #We don't guarantee that existing tokens will be + #usable after a migration + token_table.delete() + token_table.drop_column( + sqlalchemy.Column('trust_id', + sqlalchemy.String(64), + nullable=True)) + token_table.drop_column( + sqlalchemy.Column('user_id', + sqlalchemy.String(64))) + + +def create_column_forgiving(migrate_engine, table, column): + try: + table.create_column(column) + except exc.OperationalError as e: + if (e.args[0].endswith('duplicate column name: %s' % column.name) + and migrate_engine.name == "sqlite"): + #sqlite does not drop columns, so if we have already + #done a downgrade and are now upgrading, we will hit + #this: the SQLite driver previously reported success + #dropping the columns but it hasn't. + pass + else: + raise + + +def upgrade_token_table(meta, migrate_engine): + #delete old tokens, as the format has changed. + #The existing tokens will not + #support some of the list functions + + token_table = sqlalchemy.Table('token', meta, autoload=True) + token_table.delete() + + create_column_forgiving( + migrate_engine, token_table, + sqlalchemy.Column('trust_id', + sqlalchemy.String(64), + nullable=True)) + create_column_forgiving( + migrate_engine, token_table, + sqlalchemy.Column('user_id', sqlalchemy.String(64))) + + +def upgrade(migrate_engine): + meta = sqlalchemy.MetaData() + meta.bind = migrate_engine + upgrade_token_table(meta, migrate_engine) + + +def downgrade(migrate_engine): + meta = sqlalchemy.MetaData() + meta.bind = migrate_engine + downgrade_token_table_with_column_drop(meta, migrate_engine) diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 27295e86..8d361ce9 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -161,32 +161,6 @@ class Tenant(controller.V2Controller): return o -def delete_tokens_for_user(context, token_api, trust_api, user_id): - try: - #First delete tokens that could get other tokens. - for token_id in token_api.list_tokens(context, user_id): - token_api.delete_token(context, token_id) - #now delete trust tokens - for trust in trust_api.list_trusts_for_trustee(context, user_id): - token_list = token_api.list_tokens(context, user_id, - trust_id=trust['id']) - for token in token_list: - token_api.delete_token(context, token) - except exception.NotImplemented: - # The users status has been changed but tokens remain valid for - # backends that can't list tokens for users - LOG.warning(_('User %s status has changed, but existing tokens ' - 'remain valid') % user_id) - - -def delete_tokens_for_group(context, identity_api, token_api, trust_api, - group_id): - user_refs = identity_api.list_users_in_group(context, group_id) - for user in user_refs: - delete_tokens_for_user( - context, token_api, trust_api, user['id']) - - class User(controller.V2Controller): def get_user(self, context, user_id): self.assert_admin(context) @@ -243,8 +217,7 @@ class User(controller.V2Controller): if user.get('password') or not user.get('enabled', True): # If the password was changed or the user was disabled we clear tokens - delete_tokens_for_user(context, self.token_api, self.trust_api, - user_id) + self._delete_tokens_for_user(context, user_id) return {'user': self._filter_domain_id(user_ref)} def delete_user(self, context, user_id): @@ -326,7 +299,7 @@ class Role(controller.V2Controller): self.identity_api.add_role_to_user_and_project( context, user_id, tenant_id, role_id) - self.token_api.revoke_tokens(context, user_id, tenant_id) + self._delete_tokens_for_user(context, user_id) role_ref = self.identity_api.get_role(context, role_id) return {'role': role_ref} @@ -347,8 +320,7 @@ class Role(controller.V2Controller): # a user also adds them to a tenant, so we must follow up on that self.identity_api.remove_role_from_user_and_project( context, user_id, tenant_id, role_id) - delete_tokens_for_user( - context, self.token_api, self.trust_api, user_id) + self._delete_tokens_for_user(context, user_id) # COMPAT(diablo): CRUD extension def get_role_refs(self, context, user_id): @@ -390,7 +362,7 @@ class Role(controller.V2Controller): role_id = role.get('roleId') self.identity_api.add_role_to_user_and_project( context, user_id, tenant_id, role_id) - self.token_api.revoke_tokens(context, user_id, tenant_id) + self._delete_tokens_for_user(context, user_id) role_ref = self.identity_api.get_role(context, role_id) return {'role': role_ref} @@ -416,7 +388,7 @@ class Role(controller.V2Controller): context, user_id, tenant_id, role_id) roles = self.identity_api.get_roles_for_user_and_project( context, user_id, tenant_id) - self.token_api.revoke_tokens(context, user_id, tenant_id) + self._delete_tokens_for_user(context, user_id) class DomainV3(controller.V3Controller): @@ -462,17 +434,14 @@ class DomainV3(controller.V3Controller): """ # 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']) + self._delete_tokens_for_user( + context, 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']) - + self._delete_tokens_for_user( + context, user['id'], + project_id=project['id']) return DomainV3.wrap_member(context, ref) @controller.protected @@ -568,9 +537,7 @@ class UserV3(controller.V3Controller): 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=ref['id']) + self._delete_tokens_for_user(context, user_id) return UserV3.wrap_member(context, ref) @@ -580,8 +547,7 @@ class UserV3(controller.V3Controller): context, user_id, group_id) # Delete any tokens so that group membership can have an # immediate effect - delete_tokens_for_user( - context, self.token_api, self.trust_api, user_id) + self._delete_tokens_for_user(context, user_id) @controller.protected def check_user_in_group(self, context, user_id, group_id): @@ -592,8 +558,7 @@ class UserV3(controller.V3Controller): def remove_user_from_group(self, context, user_id, group_id): self.identity_api.remove_user_from_group( context, user_id, group_id) - delete_tokens_for_user( - context, self.token_api, self.trust_api, user_id) + self._delete_tokens_for_user(context, user_id) @controller.protected def delete_user(self, context, user_id): @@ -644,8 +609,7 @@ class GroupV3(controller.V3Controller): user_refs = self.identity_api.list_users_in_group(context, group_id) self.identity_api.delete_group(context, group_id) for user in user_refs: - delete_tokens_for_user( - context, self.token_api, self.trust_api, user['id']) + self._delete_tokens_for_user(context, user['id']) class CredentialV3(controller.V3Controller): @@ -738,12 +702,9 @@ class RoleV3(controller.V3Controller): # delete any tokens for this user or, in the case of a group, # tokens from all the uses who are members of this group. if user_id: - delete_tokens_for_user( - context, self.token_api, self.trust_api, user_id) + self._delete_tokens_for_user(context, user_id) else: - delete_tokens_for_group( - context, self.identity_api, self.token_api, self.trust_api, - group_id) + self._delete_tokens_for_group(context, group_id) @controller.protected def list_grants(self, context, user_id=None, group_id=None, @@ -779,9 +740,6 @@ class RoleV3(controller.V3Controller): # Now delete any tokens for this user or, in the case of a group, # tokens from all the uses who are members of this group. if user_id: - delete_tokens_for_user( - context, self.token_api, self.trust_api, user_id) + self._delete_tokens_for_user(context, user_id) else: - delete_tokens_for_group( - context, self.identity_api, self.token_api, - self.trust_api, group_id) + self._delete_tokens_for_group(context, group_id) diff --git a/keystone/token/backends/kvs.py b/keystone/token/backends/kvs.py index da45b37f..49f15ad8 100644 --- a/keystone/token/backends/kvs.py +++ b/keystone/token/backends/kvs.py @@ -45,8 +45,8 @@ class Token(kvs.Base, token.Driver): data_copy = copy.deepcopy(data) if not data_copy.get('expires'): data_copy['expires'] = token.default_expire_time() - if 'trust_id' in data and data['trust_id'] is None: - data_copy.pop('trust_id') + if not data_copy.get('user_id'): + data_copy['user_id'] = data_copy['user']['id'] self.db.set('token-%s' % token_id, data_copy) return copy.deepcopy(data_copy) diff --git a/keystone/token/backends/memcache.py b/keystone/token/backends/memcache.py index b097ab5e..8ab1f86d 100644 --- a/keystone/token/backends/memcache.py +++ b/keystone/token/backends/memcache.py @@ -66,6 +66,8 @@ class Token(token.Driver): ptk = self._prefix_token_id(token.unique_id(token_id)) if not data_copy.get('expires'): data_copy['expires'] = token.default_expire_time() + if not data_copy.get('user_id'): + data_copy['user_id'] = data_copy['user']['id'] kwargs = {} if data_copy['expires'] is not None: expires_ts = utils.unixtime(data_copy['expires']) diff --git a/keystone/token/backends/sql.py b/keystone/token/backends/sql.py index 561bb027..fef3b81b 100644 --- a/keystone/token/backends/sql.py +++ b/keystone/token/backends/sql.py @@ -26,11 +26,13 @@ from keystone import token class TokenModel(sql.ModelBase, sql.DictBase): __tablename__ = 'token' - attributes = ['id', 'expires'] + attributes = ['id', 'expires', 'user_id', 'trust_id'] id = sql.Column(sql.String(64), primary_key=True) expires = sql.Column(sql.DateTime(), default=None) extra = sql.Column(sql.JsonBlob()) valid = sql.Column(sql.Boolean(), default=True) + user_id = sql.Column(sql.String(64)) + trust_id = sql.Column(sql.String(64), nullable=True) class Token(sql.Base, token.Driver): @@ -55,6 +57,9 @@ class Token(sql.Base, token.Driver): data_copy = copy.deepcopy(data) if not data_copy.get('expires'): data_copy['expires'] = token.default_expire_time() + if not data_copy.get('user_id'): + data_copy['user_id'] = data_copy['user']['id'] + token_ref = TokenModel.from_dict(data_copy) token_ref.id = token.unique_id(token_id) token_ref.valid = True @@ -76,27 +81,20 @@ class Token(sql.Base, token.Driver): session.flush() def _list_tokens_for_trust(self, trust_id): - def trust_matches(trust_id, token_ref_dict): - return (token_ref_dict.get('trust_id') and - token_ref_dict['trust_id'] == trust_id) - session = self.get_session() tokens = [] now = timeutils.utcnow() query = session.query(TokenModel) query = query.filter(TokenModel.expires > now) + query = query.filter(TokenModel.trust_id == trust_id) + token_references = query.filter_by(valid=True) for token_ref in token_references: token_ref_dict = token_ref.to_dict() - if trust_matches(trust_id, token_ref_dict): - tokens.append(token_ref['id']) + tokens.append(token_ref['id']) return tokens def _list_tokens_for_user(self, user_id, tenant_id=None): - def user_matches(user_id, token_ref_dict): - return (token_ref_dict.get('user') and - token_ref_dict['user'].get('id') == user_id) - def tenant_matches(tenant_id, token_ref_dict): return ((tenant_id is None) or (token_ref_dict.get('tenant') and @@ -107,12 +105,13 @@ class Token(sql.Base, token.Driver): now = timeutils.utcnow() query = session.query(TokenModel) query = query.filter(TokenModel.expires > now) + query = query.filter(TokenModel.user_id == user_id) + token_references = query.filter_by(valid=True) for token_ref in token_references: token_ref_dict = token_ref.to_dict() - if (user_matches(user_id, token_ref_dict) and - tenant_matches(tenant_id, token_ref_dict)): - tokens.append(token_ref['id']) + if tenant_matches(tenant_id, token_ref_dict): + tokens.append(token_ref['id']) return tokens def list_tokens(self, user_id, tenant_id=None, trust_id=None): diff --git a/keystone/token/core.py b/keystone/token/core.py index 37ecffbc..495e295a 100644 --- a/keystone/token/core.py +++ b/keystone/token/core.py @@ -121,15 +121,6 @@ class Manager(manager.Manager): def __init__(self): super(Manager, self).__init__(CONF.token.driver) - def revoke_tokens(self, context, user_id, tenant_id=None): - """Invalidates all tokens held by a user (optionally for a tenant). - - If a specific tenant ID is not provided, *all* tokens held by user will - be revoked. - """ - for token_id in self.list_tokens(context, user_id, tenant_id): - self.delete_token(context, token_id) - class Driver(object): """Interface description for a Token driver.""" @@ -200,11 +191,3 @@ class Driver(object): """ raise exception.NotImplemented() - - def revoke_tokens(self, user_id, tenant_id=None): - """Invalidates all tokens held by a user (optionally for a tenant). - - :raises: keystone.exception.UserNotFound, - keystone.exception.ProjectNotFound - """ - raise exception.NotImplemented() diff --git a/tests/test_auth.py b/tests/test_auth.py index dd729b73..3603adcc 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -687,10 +687,8 @@ class AuthWithTrust(AuthTest): self.assert_token_count_for_trust(0) auth_response = self.fetch_v2_token_from_trust() self.assert_token_count_for_trust(1) - identity.controllers.delete_tokens_for_user( + self.trust_controller._delete_tokens_for_user( {}, - self.trust_controller.token_api, - self.trust_controller.trust_api, self.trustee['id']) self.assert_token_count_for_trust(0) diff --git a/tests/test_backend.py b/tests/test_backend.py index 6efdb17e..ce5ca258 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1953,14 +1953,18 @@ class TokenTests(object): def test_token_crud(self): token_id = uuid.uuid4().hex data = {'id': token_id, 'a': 'b', + 'trust_id': None, 'user': {'id': 'testuserid'}} data_ref = self.token_api.create_token(token_id, data) expires = data_ref.pop('expires') + data_ref.pop('user_id') self.assertTrue(isinstance(expires, datetime.datetime)) self.assertDictEqual(data_ref, data) new_data_ref = self.token_api.get_token(token_id) expires = new_data_ref.pop('expires') + new_data_ref.pop('user_id') + self.assertTrue(isinstance(expires, datetime.datetime)) self.assertEquals(new_data_ref, data) @@ -2045,8 +2049,10 @@ class TokenTests(object): expire_time = timeutils.utcnow() - datetime.timedelta(minutes=1) data = {'id_hash': token_id, 'id': token_id, 'a': 'b', 'expires': expire_time, + 'trust_id': None, 'user': {'id': 'testuserid'}} data_ref = self.token_api.create_token(token_id, data) + data_ref.pop('user_id') self.assertDictEqual(data_ref, data) self.assertRaises(exception.TokenNotFound, self.token_api.get_token, token_id) diff --git a/tests/test_sql_upgrade.py b/tests/test_sql_upgrade.py index 1ba9b419..754561c8 100644 --- a/tests/test_sql_upgrade.py +++ b/tests/test_sql_upgrade.py @@ -658,7 +658,10 @@ class SqlUpgradeTests(test.TestCase): def test_upgrade_trusts(self): self.assertEqual(self.schema.version, 0, "DB is at version 0") - self.upgrade(18) + self.upgrade(20) + self.assertTableColumns("token", + ["id", "expires", "extra", "valid"]) + self.upgrade(21) self.assertTableColumns("trust", ["id", "trustor_user_id", "trustee_user_id", @@ -667,6 +670,9 @@ class SqlUpgradeTests(test.TestCase): "expires_at", "extra"]) self.assertTableColumns("trust_role", ["trust_id", "role_id"]) + self.assertTableColumns("token", + ["id", "expires", "extra", "valid", + "trust_id", "user_id"]) def test_fixup_role(self): session = self.Session() diff --git a/tests/test_v3.py b/tests/test_v3.py index 2facfe37..2381525f 100644 --- a/tests/test_v3.py +++ b/tests/test_v3.py @@ -214,12 +214,13 @@ class RestfulTestCase(test_content_types.RestfulTestCase): def v3_request(self, path, **kwargs): # Check if the caller has passed in auth details for # use in requesting the token - auth = kwargs.get('auth', None) + auth = kwargs.pop('auth', None) if auth: - kwargs.pop('auth') token = self.get_requested_token(auth) else: - token = self.get_scoped_token() + token = kwargs.pop('token', None) + if not token: + token = self.get_scoped_token() path = '/v3' + path return self.admin_request( path=path, diff --git a/tests/test_v3_auth.py b/tests/test_v3_auth.py index f1fb1222..dea677c2 100644 --- a/tests/test_v3_auth.py +++ b/tests/test_v3_auth.py @@ -35,8 +35,8 @@ class TestAuthInfo(test_v3.RestfulTestCase): # building helper functions, they cause backend databases and fixtures # to be loaded unnecessarily. Separating out the helper functions from # this base class would improve efficiency (Bug #1134836) - def setUp(self): - super(TestAuthInfo, self).setUp(load_sample_data=False) + def setUp(self, load_sample_data=False): + super(TestAuthInfo, self).setUp(load_sample_data=load_sample_data) def test_missing_auth_methods(self): auth_data = {'identity': {}} @@ -815,9 +815,9 @@ class TestAuthXML(TestAuthJSON): content_type = 'xml' -class TestTrustAuth(test_v3.RestfulTestCase): +class TestTrustAuth(TestAuthInfo): def setUp(self): - super(TestTrustAuth, self).setUp() + super(TestTrustAuth, self).setUp(load_sample_data=True) # create a trustee to delegate stuff to self.trustee_user_id = uuid.uuid4().hex @@ -1065,3 +1065,43 @@ class TestTrustAuth(test_v3.RestfulTestCase): self.user_id, expected_status=200) trusts = r.body['trusts'] self.assertEqual(len(trusts), 0) + + def test_change_password_invalidates_trust_tokens(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + del ref['id'] + + r = self.post('/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.post('/auth/tokens', body=auth_data) + + self.assertValidProjectTrustScopedTokenResponse(r, self.user) + trust_token = r.getheader('X-Subject-Token') + + self.get('/trusts?trustor_user_id=%s' % + self.user_id, expected_status=200, + token=trust_token) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password']) + + self.assertValidUserResponse( + self.patch('/users/%s' % self.trustee_user['id'], + body={'user': {'password': uuid.uuid4().hex}}, + auth=auth_data, + expected_status=200)) + + self.get('/trusts?trustor_user_id=%s' % + self.user_id, expected_status=401, + token=trust_token) |