Clear the project ID from user information

Currently when a project is deleted, the project ID details
still exists in user information. After this fix, when a project
is deleted the default project ID in user
information will be cleared.

Closes-Bug: #1523369
Signed-off-by: Kalaswan Datta <kalaswan.datta@nectechnologies.in>

Change-Id: I3db0cf27d3cfdf6cf7c5bb34ec1b27ef80c139f4
This commit is contained in:
Kalaswan Datta 2016-01-20 00:55:29 -05:00 committed by Lance Bragstad
parent 66c8612fb1
commit 51d5597df7
10 changed files with 111 additions and 10 deletions

View File

@ -0,0 +1,15 @@
# 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.
def upgrade(migrate_engine):
pass

View File

@ -0,0 +1,15 @@
# 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.
def upgrade(migrate_engine):
pass

View File

@ -0,0 +1,21 @@
# 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 as sql
def upgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine
user = sql.Table('user', meta, autoload=True)
sql.Index('ix_default_project_id', user.c.default_project_id).create()

View File

@ -221,6 +221,15 @@ class IdentityDriverBase(object):
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def unset_default_project_id(self, project_id):
"""Unset a users default project given a specific project ID.
:param str project_id: project ID
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def list_users_in_group(self, group_id, hints):
"""List users in a group.

View File

@ -87,6 +87,11 @@ class Identity(base.IdentityDriverBase):
def list_users(self, hints):
return self.user.get_all_filtered(hints)
def unset_default_project_id(self, project_id):
# This function is not implemented for the LDAP backend
# LDAP backend is readonly.
self._disallow_write()
def get_user_by_name(self, user_name, domain_id):
# domain_id will already have been handled in the Manager layer,
# parameter left in so this matches the Driver specification

View File

@ -170,6 +170,14 @@ class Identity(base.IdentityDriverBase):
user_refs = sql.filter_limit_query(model.User, query, hints)
return [base.filter_user(x.to_dict()) for x in user_refs]
def unset_default_project_id(self, project_id):
with sql.session_for_write() as session:
query = session.query(model.User)
query = query.filter(model.User.default_project_id == project_id)
for user in query:
user.default_project_id = None
def _get_user(self, session, user_id):
user_ref = session.query(model.User).get(user_id)
if not user_ref:

View File

@ -39,7 +39,7 @@ class User(sql.ModelBase, sql.DictBase):
domain_id = sql.Column(sql.String(64), nullable=False)
_enabled = sql.Column('enabled', sql.Boolean)
extra = sql.Column(sql.JsonBlob())
default_project_id = sql.Column(sql.String(64))
default_project_id = sql.Column(sql.String(64), index=True)
_resource_option_mapper = orm.relationship(
'UserOption',
single_parent=True,

View File

@ -490,6 +490,7 @@ class Manager(manager.Manager):
self.event_callbacks = {
notifications.ACTIONS.deleted: {
'domain': [self._domain_deleted],
'project': [self._set_default_project_to_none],
},
}
@ -520,8 +521,21 @@ class Manager(manager.Manager):
'cleanup.'),
{'userid': user['id'], 'domainid': domain_id})
# Domain ID normalization methods
def _set_default_project_to_none(self, service, resource_type, operation,
payload):
"""Callback, clears user default_project_id after project deletion.
Notification approach was used instead of using a FK constraint.
Reason being, operators are allowed to have separate backends for
various keystone subsystems. This doesn't guarantee that projects and
users will be stored in the same backend, meaning we can't rely on FK
constraints to do this work for us.
"""
project_id = payload['resource_info']
self.driver.unset_default_project_id(project_id)
# Domain ID normalization methods
def _set_domain_id_and_mapping(self, ref, domain_id, driver,
entity_type):
"""Patch the domain_id/public_id into the resulting entity(ies).

View File

@ -0,0 +1,14 @@
fixes:
- |
[`bug 1523369 <https://bugs.launchpad.net/keystone/+bug/1523369>`_]
Currently, if a project is deleted, it is not removed as a user's default
project id. Now the default project id is set to none, however changes may
not be visible until memcache end of life.
upgrade:
- |
The identity backend driver interface has changed. We've added a new
``unset_default_project_id(project_id)`` method to unset a users default
project id matching the given project id. If you have a custom
implementation for the identity driver, you will need to implement this
new method.

View File

@ -1505,8 +1505,8 @@ class ResourceTests(object):
project_ref = self.resource_api.get_project(project['id'])
self.assertDictEqual(updated_project_ref, project_ref)
@test_utils.wip('waiting for fix to bug #1523369')
def test_delete_project_clears_default_project_id(self):
self.config_fixture.config(group='cache', enabled=False)
project = unit.new_project_ref(
domain_id=CONF.identity.default_domain_id)
user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id,
@ -1514,13 +1514,14 @@ class ResourceTests(object):
self.resource_api.create_project(project['id'], project)
user = self.identity_api.create_user(user)
user = self.identity_api.get_user(user['id'])
self.assertIsNotNone(user['default_project_id'])
self.resource_api.delete_project(project['id'])
user = self.identity_api.get_user(user['id'])
self.assertIsNone(user['default_project_id'])
# LDAP is read only default_project_id doesn't exist
if 'default_project_id' in user:
self.assertIsNotNone(user['default_project_id'])
self.resource_api.delete_project(project['id'])
user = self.identity_api.get_user(user['id'])
self.assertNotIn('default_project_id', user)
@test_utils.wip('waiting for fix to bug #1523369')
def test_delete_project_with_roles_clears_default_project_id(self):
project = unit.new_project_ref(
domain_id=CONF.identity.default_domain_id)
@ -1533,10 +1534,9 @@ class ResourceTests(object):
self.assignment_api.create_grant(user_id=user['id'],
project_id=project['id'],
role_id=role['id'])
self.resource_api.delete_project(project['id'])
user = self.identity_api.get_user(user['id'])
self.assertIsNone(user['default_project_id'])
self.assertNotIn('default_project_id', user)
class ResourceDriverTests(object):