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:
parent
d7849bde40
commit
a272c8b422
|
@ -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)
|
|
@ -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']
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue