Shadow LDAP and custom driver users

This patch creates a new shadow table for mapping non-local
users to local identities. By non-local, I mean non-sql and
non-federated, so essentially, LDAP and custom driver identities.

user -> local_user (sql driver)
     -> nonlocal_user (ldap, custom driver)
     -> federated_user

"One user to rule them all"

bp shadow-users-newton

Change-Id: I4d1360bf462556ba1be33e1015d0f17368ebdead
This commit is contained in:
Ronald De Rose 2016-05-31 21:56:32 +00:00 committed by Ron De Rose
parent d7849bde40
commit a272c8b422
8 changed files with 181 additions and 4 deletions

View File

@ -0,0 +1,32 @@
# 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_table = sql.Table('user', meta, autoload=True)
nonlocal_user_table = sql.Table(
'nonlocal_user',
meta,
sql.Column('domain_id', sql.String(64), primary_key=True),
sql.Column('name', sql.String(255), primary_key=True),
sql.Column('user_id', sql.String(64),
sql.ForeignKey(user_table.c.id, ondelete='CASCADE'),
nullable=False),
mysql_engine='InnoDB',
mysql_charset='utf8')
nonlocal_user_table.create(migrate_engine, checkfirst=True)

View File

@ -35,12 +35,19 @@ class User(sql.ModelBase, sql.DictBase):
lazy='subquery',
cascade='all,delete-orphan',
backref='user')
nonlocal_users = orm.relationship('NonLocalUser',
single_parent=True,
lazy='subquery',
cascade='all,delete-orphan',
backref='user')
# name property
@hybrid_property
def name(self):
if self.local_user:
return self.local_user.name
elif self.nonlocal_users:
return self.nonlocal_users[0].name
elif self.federated_users:
return self.federated_users[0].display_name
else:
@ -85,6 +92,8 @@ class User(sql.ModelBase, sql.DictBase):
def domain_id(self):
if self.local_user:
return self.local_user.domain_id
elif self.nonlocal_users:
return self.nonlocal_users[0].domain_id
else:
return None
@ -148,6 +157,17 @@ class FederatedUser(sql.ModelBase, sql.ModelDictMixin):
)
class NonLocalUser(sql.ModelBase, sql.ModelDictMixin):
"""SQL data model for nonlocal users (LDAP and custom)."""
__tablename__ = 'nonlocal_user'
attributes = ['domain_id', 'name', 'user_id']
domain_id = sql.Column(sql.String(64), primary_key=True)
name = sql.Column(sql.String(255), primary_key=True)
user_id = sql.Column(sql.String(64), sql.ForeignKey('user.id',
ondelete='CASCADE'))
class Group(sql.ModelBase, sql.DictBase):
__tablename__ = 'group'
attributes = ['id', 'name', 'domain_id', 'description']

View File

@ -825,8 +825,9 @@ class Manager(manager.Manager):
domain_id, driver, entity_id = (
self._get_domain_driver_and_entity_id(user_id))
ref = driver.authenticate(entity_id, password)
return self._set_domain_id_and_mapping(
ref = self._set_domain_id_and_mapping(
ref, domain_id, driver, mapping.EntityType.USER)
return self._shadow_nonlocal_user(ref)
def _assert_default_project_id_is_not_domain(self, default_project_id):
if default_project_id:
@ -1225,6 +1226,13 @@ class Manager(manager.Manager):
update_dict = {'password': new_password}
self.update_user(user_id, update_dict)
@MEMOIZE
def _shadow_nonlocal_user(self, user):
try:
return self.shadow_users_api.get_user(user['id'])
except exception.UserNotFound:
return self.shadow_users_api.create_nonlocal_user(user)
@MEMOIZE
def shadow_federated_user(self, idp_id, protocol_id, unique_id,
display_name):
@ -1295,7 +1303,16 @@ class ShadowUsersManager(manager.Manager):
driver_namespace = 'keystone.identity.shadow_users'
def __init__(self):
super(ShadowUsersManager, self).__init__(CONF.shadow_users.driver)
shadow_driver = CONF.shadow_users.driver
super(ShadowUsersManager, self).__init__(shadow_driver)
if isinstance(self.driver, shadow_interface.ShadowUsersDriverV9):
self.driver = (
shadow_interface.V10ShadowUsersWrapperForV9Driver(self.driver))
elif not isinstance(self.driver,
shadow_interface.ShadowUsersDriverV10):
raise exception.UnsupportedDriverVersion(driver=shadow_driver)
@versionutils.deprecated(

View File

@ -14,13 +14,14 @@
import abc
from oslo_log import versionutils
import six
from keystone import exception
@six.add_metaclass(abc.ABCMeta)
class ShadowUsersDriverV9(object):
class ShadowUsersDriverBase(object):
"""Interface description for an Shadow Users driver."""
@abc.abstractmethod
@ -58,3 +59,45 @@ class ShadowUsersDriverV9(object):
"""
raise exception.NotImplemented()
@versionutils.deprecated(
versionutils.deprecated.NEWTON,
what='keystone.identity.shadow_backends.base.ShadowUsersDriverV9',
in_favor_of='keystone.identity.shadow_backends.base.ShadowUsersDriverV10',
remove_in=+1)
class ShadowUsersDriverV9(ShadowUsersDriverBase):
pass
@six.add_metaclass(abc.ABCMeta)
class ShadowUsersDriverV10(ShadowUsersDriverBase):
"""Interface description for an Shadow Users V10 driver."""
@abc.abstractmethod
def get_user(self, user_id):
"""Return the found user.
:param user_id: Identity of the user
:returns dict: Containing the user reference
"""
raise exception.NotImplemented()
@abc.abstractmethod
def create_nonlocal_user(self, user_dict):
"""Create a new non-local user.
:param dict user_dict: Reference to the non-local user
:returns dict: Containing the user reference
"""
raise exception.NotImplemented()
class V10ShadowUsersWrapperForV9Driver(ShadowUsersDriverV10):
def get_user(self, user_id):
raise exception.UserNotFound(user_id=user_id)
def create_nonlocal_user(self, user_dict):
return user_dict

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import uuid
from keystone.common import sql
@ -19,7 +20,7 @@ from keystone.identity.backends import sql_model as model
from keystone.identity.shadow_backends import base
class ShadowUsers(base.ShadowUsersDriverV9):
class ShadowUsers(base.ShadowUsersDriverV10):
@sql.handle_conflicts(conflict_type='federated_user')
def create_federated_user(self, federated_dict):
user = {
@ -72,3 +73,29 @@ class ShadowUsers(base.ShadowUsersDriverV9):
display_name)
query.update({'display_name': display_name})
return
@sql.handle_conflicts(conflict_type='nonlocal_user')
def create_nonlocal_user(self, user_dict):
new_user_dict = copy.deepcopy(user_dict)
new_nonlocal_user_dict = {
'domain_id': user_dict['domain_id'],
'name': user_dict['name']
}
with sql.session_for_write() as session:
new_nonlocal_user_ref = model.NonLocalUser.from_dict(
new_nonlocal_user_dict)
new_user_ref = model.User.from_dict(new_user_dict)
new_user_ref.nonlocal_users.append(new_nonlocal_user_ref)
session.add(new_user_ref)
return identity_base.filter_user(new_user_ref.to_dict())
def get_user(self, user_id):
with sql.session_for_read() as session:
user_ref = self._get_user(session, user_id)
return identity_base.filter_user(user_ref.to_dict())
def _get_user(self, session, user_id):
user_ref = session.query(model.User).get(user_id)
if not user_ref:
raise exception.UserNotFound(user_id=user_id)
return user_ref

View File

@ -1360,6 +1360,28 @@ class LimitTests(filtering.FilterTests):
class ShadowUsersTests(object):
def test_create_nonlocal_user_unique_constraint(self):
user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id)
user_created = self.shadow_users_api.create_nonlocal_user(user)
self.assertNotIn('password', user_created)
self.assertEqual(user_created['id'], user['id'])
self.assertEqual(user_created['domain_id'], user['domain_id'])
self.assertEqual(user_created['name'], user['name'])
new_user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id)
new_user['name'] = user['name']
self.assertRaises(exception.Conflict,
self.shadow_users_api.create_nonlocal_user,
new_user)
def test_get_user(self):
user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id)
user.pop('email')
user.pop('password')
user_created = self.shadow_users_api.create_nonlocal_user(user)
self.assertEqual(user_created['id'], user['id'])
user_found = self.shadow_users_api.get_user(user_created['id'])
self.assertItemsEqual(user_created, user_found)
def test_create_federated_user_unique_constraint(self):
federated_dict = unit.new_federated_user_ref()
user_dict = self.shadow_users_api.create_federated_user(federated_dict)

View File

@ -157,6 +157,12 @@ class SqlModels(SqlTests):
('display_name', sql.String, 255))
self.assertExpectedSchema('federated_user', cols)
def test_nonlocal_user_model(self):
cols = (('domain_id', sql.String, 64),
('name', sql.String, 255),
('user_id', sql.String, 64))
self.assertExpectedSchema('nonlocal_user', cols)
def test_group_model(self):
cols = (('id', sql.String, 64),
('name', sql.String, 64),

View File

@ -1162,6 +1162,16 @@ class SqlUpgradeTests(SqlMigrateBase):
self.upgrade(102)
self.assertTableDoesNotExist('domain')
def test_add_nonlocal_user_table(self):
nonlocal_user_table = 'nonlocal_user'
self.upgrade(102)
self.assertTableDoesNotExist(nonlocal_user_table)
self.upgrade(103)
self.assertTableColumns(nonlocal_user_table,
['domain_id',
'name',
'user_id'])
class MySQLOpportunisticUpgradeTestCase(SqlUpgradeTests):
FIXTURE = test_base.MySQLOpportunisticFixture