Code-Defined Resource-specific Options

Implement the code-defined resource specific options framework
for the Identity subsystem (User). Options are defined in code
in the keystone.identity.backends.resource_options. These options
are extracted from the update dict, validated for a specific type,
and stored in an attribute association (as a one[User] to
Many[one-per-option-type]) relation in a database table.

On retrival options are converted from the association to elements
in the user reference dict.

Setting an option explicitly to None removes the option (unsets it).

Updating a specific option will have no impact on other options.

Change-Id: I90875ce2c3cf899557bb1f215fa1bda9576cbe83
This commit is contained in:
Morgan Fainberg 2017-01-23 13:28:00 -08:00
parent c19f243152
commit 1896d1ba0d
15 changed files with 635 additions and 6 deletions

View File

@ -0,0 +1,193 @@
# 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.
"""Options specific to resources managed by Keystone (Domain, User, etc)."""
import six
from keystone.i18n import _
def _validator(value):
return
def ref_mapper_to_dict_options(ref):
"""Convert the values in _resource_option_mapper to options dict.
NOTE: this is to be called from the relevant `to_dict` methods or
similar and must be called from within the active session context.
:param ref: the DB model ref to extract options from
:returns: Dict of options as expected to be returned out of to_dict in
the `options` key.
"""
options = {}
for opt in ref._resource_option_mapper.values():
if opt.option_id in ref.resource_options_registry.option_ids:
r_opt = ref.resource_options_registry.get_option_by_id(
opt.option_id)
if r_opt is not None:
options[r_opt.option_name] = opt.option_value
return options
def resource_options_ref_to_mapper(ref, option_class):
"""Convert the _resource_options property-dict to options attr map.
The model must have the resource option mapper located in the
``_resource_option_mapper`` attribute.
The model must have the resource option registry located in the
``resource_options_registry` attribute.
The option dict with key(opt_id), value(opt_value) will be pulled from
``ref._resource_options``.
NOTE: This function MUST be called within the active writer session
context!
:param ref: The DB model reference that is actually stored to the
backend.
:param option_class: Class that is used to store the resource option
in the DB.
"""
options = getattr(ref, '_resource_options', None)
if options is not None:
# To ensure everything is clean, no lingering refs.
delattr(ref, '_resource_options')
else:
# _resource_options didn't exist. Work from an empty set.
options = {}
# NOTE(notmorgan): explicitly use .keys() here as the attribute mapper
# has some oddities at times. This guarantees we are working with keys.
set_options = set(ref._resource_option_mapper.keys())
# Get any options that are not registered and slate them for removal from
# the DB. This will delete unregistered options.
clear_options = set_options.difference(
ref.resource_options_registry.option_ids)
options.update({x: None for x in clear_options})
# Set the resource options for user in the Attribute Mapping.
for r_opt_id, r_opt_value in options.items():
if r_opt_value is None:
# Delete any option set explicitly to None, ignore unset
# options.
ref._resource_option_mapper.pop(r_opt_id, None)
else:
# Set any options on the user_ref itself.
opt_obj = option_class(
option_id=r_opt_id,
option_value=r_opt_value)
ref._resource_option_mapper[r_opt_id] = opt_obj
class ResourceOptionRegistry(object):
def __init__(self, registry_name):
self._registered_options = {}
self._registry_type = registry_name
@property
def option_names(self):
return set([opt.option_name for opt in self.options])
@property
def options_by_name(self):
return {opt.option_name: opt
for opt in self._registered_options.values()}
@property
def options(self):
return self._registered_options.values()
@property
def option_ids(self):
return set(self._registered_options.keys())
def get_option_by_id(self, opt_id):
return self._registered_options.get(opt_id, None)
def get_option_by_name(self, name):
for option in self._registered_options.values():
if name == option.option_name:
return option
return None
def register_option(self, option):
if option in self.options:
# Re-registering the exact same option does nothing.
return
if option.option_id in self._registered_options:
raise ValueError(_('Option %(option_id)s already defined in '
'%(registry)s.') %
{'option_id': option.option_id,
'registry': self._registry_type})
if option.option_name in self.option_names:
raise ValueError(_('Option %(option_name)s already defined in '
'%(registry)s') %
{'option_name': option.option_name,
'registry': self._registry_type})
self._registered_options[option.option_id] = option
class ResourceOption(object):
def __init__(self, option_id, option_name, validator=_validator):
"""The base object to define the option(s) to be stored in the DB.
:param option_id: The ID of the option. This will be used to lookup
the option value from the DB and should not be
changed once defined as the values will no longer
be correctly mapped to the keys in the user_ref when
retrieving the data from the DB.
:type option_id: str
:param option_name: The name of the option. This value will be used
to map the value from the user request on a
resource update to the correct option id to be
stored in the database. This value should not be
changed once defined as it will change the
resulting keys in the user_ref.
:type option_name: str
:param validator: A callable that raises TypeError if the value to be
persisted is incorrect. A single argument of the
value to be persisted will be passed to it. No return
value is expected.
:type validator: callable
"""
if not isinstance(option_id, six.string_types) and len(option_id) == 4:
raise TypeError(_('`option_id` must be a string, got %r')
% option_id)
elif len(option_id) != 4:
raise ValueError(_('`option_id` must be 4 characters in '
'length. Got %r') % option_id)
if not isinstance(option_name, six.string_types):
raise TypeError(_('`option_name` must be a string. '
'Got %r') % option_name)
self._option_id = option_id
self._option_name = option_name
self.validator = validator
@property
def option_name(self):
# NOTE(notmorgan) Option IDs should never be set outside of definition
# time.
return self._option_name
@property
def option_id(self):
# NOTE(notmorgan) Option IDs should never be set outside of definition
# time.
return self._option_id

View File

@ -0,0 +1,16 @@
# 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):
# NOTE(notmorgan): This is a no-op, no data-migration needed.
pass

View File

@ -0,0 +1,16 @@
# 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):
# NOTE(notmorgan): This is a no-op, no data-migration needed.
pass

View File

@ -0,0 +1,34 @@
# 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
from keystone.common import sql as ks_sql
def upgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine
user_table = sql.Table('user', meta, autoload=True)
user_option = sql.Table(
'user_option',
meta,
sql.Column('user_id', sql.String(64), sql.ForeignKey(user_table.c.id,
ondelete='CASCADE'), nullable=False, primary_key=True),
sql.Column('option_id', sql.String(4), nullable=False,
primary_key=True),
sql.Column('option_value', ks_sql.JsonBlob, nullable=True),
mysql_engine='InnoDB',
mysql_charset='utf8')
user_option.create(migrate_engine, checkfirst=True)

View File

@ -0,0 +1,34 @@
# 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.
from keystone.common import resource_options
USER_OPTIONS_REGISTRY = resource_options.ResourceOptionRegistry('USER')
USER_OPTIONS_LIST = [
# NOTE(notmorgan): This placeholder options can be removed once more
# options are populated. This forces iteration on possible options for
# complete test purposes in unit/functional/gate tests outside of the
# explicit test cases that test resource options. This option is never
# expected to be set.
resource_options.ResourceOption('_TST', '__PLACEHOLDER__'),
]
# NOTE(notmorgan): wrap this in a function for testing purposes.
# This is called on import by design.
def register_user_options():
for opt in USER_OPTIONS_LIST:
USER_OPTIONS_REGISTRY.register_option(opt)
register_user_options()

View File

@ -18,6 +18,7 @@ from oslo_db import api as oslo_db_api
import sqlalchemy
from keystone.common import driver_hints
from keystone.common import resource_options
from keystone.common import sql
from keystone.common import utils
import keystone.conf
@ -125,6 +126,9 @@ class Identity(base.IdentityDriverBase):
user_ref = model.User.from_dict(user)
user_ref.created_at = datetime.datetime.utcnow()
session.add(user_ref)
# Set resource options passed on creation
resource_options.resource_options_ref_to_mapper(
user_ref, model.UserOption)
return base.filter_user(user_ref.to_dict())
def _create_password_expires_query(self, session, query, hints):
@ -188,6 +192,16 @@ class Identity(base.IdentityDriverBase):
for attr in model.User.attributes:
if attr not in model.User.readonly_attributes:
setattr(user_ref, attr, getattr(new_user, attr))
# Move the "_resource_options" attribute over to the real user_ref
# so that resource_options.resource_options_ref_to_mapper can
# handle the work.
setattr(user_ref, '_resource_options',
getattr(new_user, '_resource_options', {}))
# Move options into the proper attribute mapper construct
resource_options.resource_options_ref_to_mapper(
user_ref, model.UserOption)
user_ref.extra = new_user.extra
return base.filter_user(
user_ref.to_dict(include_extra_dict=True))

View File

@ -17,9 +17,12 @@ import datetime
import sqlalchemy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import orm
from sqlalchemy.orm import collections
from keystone.common import resource_options
from keystone.common import sql
import keystone.conf
from keystone.identity.backends import resource_options as iro
CONF = keystone.conf.CONF
@ -30,11 +33,19 @@ class User(sql.ModelBase, sql.DictBase):
attributes = ['id', 'name', 'domain_id', 'password', 'enabled',
'default_project_id', 'password_expires_at']
readonly_attributes = ['id', 'password_expires_at']
resource_options_registry = iro.USER_OPTIONS_REGISTRY
id = sql.Column(sql.String(64), primary_key=True)
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))
_resource_option_mapper = orm.relationship(
'UserOption',
single_parent=True,
cascade='all,delete,delete-orphan',
lazy='subquery',
backref='user',
collection_class=collections.attribute_mapped_collection('option_id'))
local_user = orm.relationship('LocalUser', uselist=False,
single_parent=True, lazy='subquery',
cascade='all,delete-orphan', backref='user')
@ -184,6 +195,9 @@ class User(sql.ModelBase, sql.DictBase):
d = super(User, self).to_dict(include_extra_dict=include_extra_dict)
if 'default_project_id' in d and d['default_project_id'] is None:
del d['default_project_id']
# NOTE(notmorgan): Eventually it may make sense to drop the empty
# option dict creation to the superclass (if enough models use it)
d['options'] = resource_options.ref_mapper_to_dict_options(self)
return d
@classmethod
@ -199,10 +213,19 @@ class User(sql.ModelBase, sql.DictBase):
"""
new_dict = user_dict.copy()
resource_options = {}
options = new_dict.pop('options', {})
password_expires_at_key = 'password_expires_at'
if password_expires_at_key in user_dict:
del new_dict[password_expires_at_key]
return super(User, cls).from_dict(new_dict)
for opt in cls.resource_options_registry.options:
if opt.option_name in options:
opt_value = options[opt.option_name]
opt.validator(opt_value)
resource_options[opt.option_id] = opt_value
user_obj = super(User, cls).from_dict(new_dict)
setattr(user_obj, '_resource_options', resource_options)
return user_obj
class LocalUser(sql.ModelBase, sql.DictBase):
@ -303,3 +326,17 @@ class UserGroupMembership(sql.ModelBase, sql.DictBase):
group_id = sql.Column(sql.String(64),
sql.ForeignKey('group.id'),
primary_key=True)
class UserOption(sql.ModelBase):
__tablename__ = 'user_option'
user_id = sql.Column(sql.String(64), sql.ForeignKey('user.id',
ondelete='CASCADE'), nullable=False,
primary_key=True)
option_id = sql.Column(sql.String(4), nullable=False,
primary_key=True)
option_value = sql.Column(sql.JsonBlob, nullable=True)
def __init__(self, option_id, option_value):
self.option_id = option_id
self.option_value = option_value

View File

@ -0,0 +1,77 @@
# 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 uuid
from keystone.common import resource_options
from keystone.tests import unit
class TestResourceOptionObjects(unit.BaseTestCase):
def test_option_init_validation(self):
# option_name must be a string
self.assertRaises(TypeError,
resource_options.ResourceOption, 'test', 1234)
# option_id must be a string
self.assertRaises(TypeError,
resource_options.ResourceOption, 1234, 'testing')
# option_id must be 4 characters
self.assertRaises(ValueError,
resource_options.ResourceOption,
'testing',
'testing')
resource_options.ResourceOption('test', 'testing')
def test_duplicate_option_cases(self):
option_id_str_valid = 'test'
registry = resource_options.ResourceOptionRegistry(option_id_str_valid)
option_name_unique = uuid.uuid4().hex
option = resource_options.ResourceOption(
option_id_str_valid, option_name_unique)
option_dup_id = resource_options.ResourceOption(
option_id_str_valid, uuid.uuid4().hex)
option_dup_name = resource_options.ResourceOption(
uuid.uuid4().hex[:4], option_name_unique)
registry.register_option(option)
self.assertRaises(ValueError, registry.register_option, option_dup_id)
self.assertRaises(ValueError, registry.register_option,
option_dup_name)
self.assertIs(1, len(registry.options))
registry.register_option(option)
self.assertIs(1, len(registry.options))
def test_registry(self):
option = resource_options.ResourceOption(uuid.uuid4().hex[:4],
uuid.uuid4().hex)
option2 = resource_options.ResourceOption(uuid.uuid4().hex[:4],
uuid.uuid4().hex)
registry = resource_options.ResourceOptionRegistry('TEST')
registry.register_option(option)
self.assertIn(option.option_name, registry.option_names)
self.assertIs(1, len(registry.options))
self.assertIn(option.option_id, registry.option_ids)
registry.register_option(option2)
self.assertIn(option2.option_name, registry.option_names)
self.assertIs(2, len(registry.options))
self.assertIn(option2.option_id, registry.option_ids)
self.assertIs(option,
registry.get_option_by_id(option.option_id))
self.assertIs(option2,
registry.get_option_by_id(option2.option_id))
self.assertIs(option,
registry.get_option_by_name(option.option_name))
self.assertIs(option2,
registry.get_option_by_name(option2.option_name))

View File

@ -848,6 +848,24 @@ class TestCase(BaseTestCase):
excName = str(expected_exception)
raise self.failureException("%s not raised" % excName)
def assertUserDictEqual(self, expected, observed, message=''):
"""Assert that a user dict is equal to another user dict.
User dictionaries have some variable values that should be ignored in
the comparison. This method is a helper that strips those elements out
when comparing the user dictionary. This normalized these differences
that should not change the comparison.
"""
# NOTE(notmorgan): An empty option list is the same as no options being
# specified in the user_ref. This removes options if it is empty in
# observed if options is not specified in the expected value.
if ('options' in observed and not observed['options'] and
'options' not in expected):
observed = observed.copy()
del observed['options']
self.assertDictEqual(expected, observed, message)
@property
def ipv6_enabled(self):
if socket.has_ipv6:

View File

@ -111,7 +111,8 @@ class IdentityDriverTests(object):
'password': uuid.uuid4().hex,
'enabled': True,
'default_project_id': uuid.uuid4().hex,
'password_expires_at': None
'password_expires_at': None,
'options': {}
}
if self.driver.is_domain_aware():
user['domain_id'] = uuid.uuid4().hex

View File

@ -21,7 +21,7 @@ class ShadowUsersCoreTests(object):
self.federated_user['unique_id'],
self.federated_user['display_name'])
self.assertIsNotNone(user['id'])
self.assertEqual(5, len(user.keys()))
self.assertEqual(6, len(user.keys()))
self.assertIsNotNone(user['name'])
self.assertIsNone(user['password_expires_at'])
self.assertIsNotNone(user['domain_id'])

View File

@ -16,11 +16,13 @@ import uuid
import freezegun
from keystone.common import controller
from keystone.common import resource_options
from keystone.common import sql
from keystone.common import utils
import keystone.conf
from keystone import exception
from keystone.identity.backends import base
from keystone.identity.backends import resource_options as iro
from keystone.identity.backends import sql_model as model
from keystone.tests.unit import test_backend_sql
@ -28,6 +30,180 @@ from keystone.tests.unit import test_backend_sql
CONF = keystone.conf.CONF
class UserResourceOptionTests(test_backend_sql.SqlTests):
def setUp(self):
super(UserResourceOptionTests, self).setUp()
# RESET STATE OF REGISTRY OPTIONS
self.addCleanup(iro.register_user_options)
self.addCleanup(iro.USER_OPTIONS_REGISTRY._registered_options.clear)
self.option1 = resource_options.ResourceOption('opt1', 'option1')
self.option2 = resource_options.ResourceOption('opt2', 'option2')
self.cleanup_instance('option1', 'option2')
iro.USER_OPTIONS_REGISTRY._registered_options.clear()
iro.USER_OPTIONS_REGISTRY.register_option(self.option1)
iro.USER_OPTIONS_REGISTRY.register_option(self.option2)
def test_user_set_option_in_resource_option(self):
user = self._create_user(self._get_user_dict())
opt_value = uuid.uuid4().hex
user['options'][self.option1.option_name] = opt_value
new_ref = self.identity_api.update_user(user['id'], user)
self.assertEqual(opt_value,
new_ref['options'][self.option1.option_name])
raw_ref = self._get_user_ref(user['id'])
self.assertIn(self.option1.option_id, raw_ref._resource_option_mapper)
self.assertEqual(
opt_value,
raw_ref._resource_option_mapper[
self.option1.option_id].option_value)
api_get_ref = self.identity_api.get_user(user['id'])
# Ensure options are properly set in a .get_user call.
self.assertEqual(opt_value,
api_get_ref['options'][self.option1.option_name])
def test_user_add_update_delete_option_in_resource_option(self):
user = self._create_user(self._get_user_dict())
opt_value = uuid.uuid4().hex
new_opt_value = uuid.uuid4().hex
# Update user to add the new value option
user['options'][self.option1.option_name] = opt_value
new_ref = self.identity_api.update_user(user['id'], user)
self.assertEqual(opt_value,
new_ref['options'][self.option1.option_name])
# Update the option Value and confirm it is updated
user['options'][self.option1.option_name] = new_opt_value
new_ref = self.identity_api.update_user(user['id'], user)
self.assertEqual(new_opt_value,
new_ref['options'][self.option1.option_name])
# Set the option value to None, meaning delete the option
user['options'][self.option1.option_name] = None
new_ref = self.identity_api.update_user(user['id'], user)
self.assertNotIn(self.option1.option_name, new_ref['options'])
def test_user_add_delete_resource_option_existing_option_values(self):
user = self._create_user(self._get_user_dict())
opt_value = uuid.uuid4().hex
opt2_value = uuid.uuid4().hex
# Update user to add the new value option
user['options'][self.option1.option_name] = opt_value
new_ref = self.identity_api.update_user(user['id'], user)
self.assertEqual(opt_value,
new_ref['options'][self.option1.option_name])
# Update the option value for option 2 and confirm it is updated and
# option1's value remains the same. Option 1 is not specified in the
# updated user ref.
del user['options'][self.option1.option_name]
user['options'][self.option2.option_name] = opt2_value
new_ref = self.identity_api.update_user(user['id'], user)
self.assertEqual(opt_value,
new_ref['options'][self.option1.option_name])
self.assertEqual(opt2_value,
new_ref['options'][self.option2.option_name])
raw_ref = self._get_user_ref(user['id'])
self.assertEqual(
opt_value,
raw_ref._resource_option_mapper[
self.option1.option_id].option_value)
self.assertEqual(
opt2_value,
raw_ref._resource_option_mapper[
self.option2.option_id].option_value)
# Set the option value to None, meaning delete the option, ensure
# option 2 still remains and has the right value
user['options'][self.option1.option_name] = None
new_ref = self.identity_api.update_user(user['id'], user)
self.assertNotIn(self.option1.option_name, new_ref['options'])
self.assertEqual(opt2_value,
new_ref['options'][self.option2.option_name])
raw_ref = self._get_user_ref(user['id'])
self.assertNotIn(raw_ref._resource_option_mapper,
self.option1.option_id)
self.assertEqual(
opt2_value,
raw_ref._resource_option_mapper[
self.option2.option_id].option_value)
def test_unregistered_resource_option_deleted(self):
user = self._create_user(self._get_user_dict())
opt_value = uuid.uuid4().hex
opt2_value = uuid.uuid4().hex
# Update user to add the new value option
user['options'][self.option1.option_name] = opt_value
new_ref = self.identity_api.update_user(user['id'], user)
self.assertEqual(opt_value,
new_ref['options'][self.option1.option_name])
# Update the option value for option 2 and confirm it is updated and
# option1's value remains the same. Option 1 is not specified in the
# updated user ref.
del user['options'][self.option1.option_name]
user['options'][self.option2.option_name] = opt2_value
new_ref = self.identity_api.update_user(user['id'], user)
self.assertEqual(opt_value,
new_ref['options'][self.option1.option_name])
self.assertEqual(opt2_value,
new_ref['options'][self.option2.option_name])
raw_ref = self._get_user_ref(user['id'])
self.assertEqual(
opt_value,
raw_ref._resource_option_mapper[
self.option1.option_id].option_value)
self.assertEqual(
opt2_value,
raw_ref._resource_option_mapper[
self.option2.option_id].option_value)
# clear registered options and only re-register option1, update user
# and confirm option2 is gone from the ref and returned dict
iro.USER_OPTIONS_REGISTRY._registered_options.clear()
iro.USER_OPTIONS_REGISTRY.register_option(self.option1)
user['name'] = uuid.uuid4().hex
new_ref = self.identity_api.update_user(user['id'], user)
self.assertNotIn(self.option2.option_name, new_ref['options'])
self.assertEqual(opt_value,
new_ref['options'][self.option1.option_name])
raw_ref = self._get_user_ref(user['id'])
self.assertNotIn(raw_ref._resource_option_mapper,
self.option2.option_id)
self.assertEqual(
opt_value,
raw_ref._resource_option_mapper[
self.option1.option_id].option_value)
def _get_user_ref(self, user_id):
with sql.session_for_read() as session:
return session.query(model.User).get(user_id)
def _create_user(self, user_dict):
user_dict['id'] = uuid.uuid4().hex
user_dict = utils.hash_user_password(user_dict)
with sql.session_for_write() as session:
user_ref = model.User.from_dict(user_dict)
session.add(user_ref)
return base.filter_user(user_ref.to_dict())
def _get_user_dict(self):
user = {
'name': uuid.uuid4().hex,
'domain_id': CONF.identity.default_domain_id,
'enabled': True,
'password': uuid.uuid4().hex
}
return user
class DisableInactiveUserTests(test_backend_sql.SqlTests):
def setUp(self):
super(DisableInactiveUserTests, self).setUp()

View File

@ -64,7 +64,7 @@ class IdentityTests(object):
# not be returned by the api
self.user_sna.pop('password')
self.user_sna['enabled'] = True
self.assertDictEqual(self.user_sna, user_ref)
self.assertUserDictEqual(self.user_sna, user_ref)
def test_authenticate_and_get_roles_no_metadata(self):
user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id)

View File

@ -181,7 +181,7 @@ class LdapPoolCommonTestMixin(object):
self.user_sna.pop('password')
self.user_sna['enabled'] = True
self.assertDictEqual(self.user_sna, user_ref)
self.assertUserDictEqual(self.user_sna, user_ref)
new_password = 'new_password'
user_ref['password'] = new_password
@ -195,7 +195,7 @@ class LdapPoolCommonTestMixin(object):
password=new_password)
user_ref.pop('password')
self.assertDictEqual(user_ref, user_ref2)
self.assertUserDictEqual(user_ref, user_ref2)
# Authentication with old password would not work here as there
# is only one connection in pool which get bind again with updated

View File

@ -2213,6 +2213,19 @@ class FullMigration(SqlMigrateBase, unit.TestCase):
user_table = sqlalchemy.Table('user', self.metadata, autoload=True)
self.assertFalse(user_table.c.domain_id.nullable)
def test_migration_016_add_user_options(self):
self.expand(15)
self.migrate(15)
self.contract(15)
user_option = 'user_option'
self.assertTableDoesNotExist(user_option)
self.expand(16)
self.migrate(16)
self.contract(16)
self.assertTableColumns(user_option,
['user_id', 'option_id', 'option_value'])
class MySQLOpportunisticFullMigration(FullMigration):
FIXTURE = test_base.MySQLOpportunisticFixture