Merge "Implement domain specific Identity backends"

This commit is contained in:
Jenkins 2013-08-16 17:34:56 +00:00 committed by Gerrit Code Review
commit 81534a182a
24 changed files with 1045 additions and 407 deletions

View File

@ -96,6 +96,25 @@ order:
PasteDeploy configuration file is specified by the ``config_file`` parameter in ``[paste_deploy]`` section of the primary configuration file. If the parameter
is not an absolute path, then Keystone looks for it in the same directories as above. If not specified, WSGI pipeline definitions are loaded from the primary configuration file.
Keystone supports the option (disabled by default) to specify identity driver
configurations on a domain by domain basis, allowing, for example, a specific
domain to have its own LDAP or SQL server. This is configured by specifying the
following options::
[identity]
domain_specific_drivers_enabled = True
domain_config_dir = /etc/keystone/domains
Setting ``domain_specific_drivers_enabled`` to True will enable this feature, causing
keystone to look in the ``domain_config_dir`` for config files of the form::
keystone.<domain_name>.conf
Options given in the domain specific configuration file will override those in the
primary configuration file for the specified domain only. Domains without a specific
configuration file will continue to use the options from the primary configuration
file.
Authentication Plugins
----------------------

View File

@ -99,6 +99,14 @@
# There is nothing special about this domain, other than the fact that it must
# exist to order to maintain support for your v2 clients.
# default_domain_id = default
#
# A subset (or all) of domains can have their own identity driver, each with
# their own partial configuration file in a domain configuration directory.
# Only values specific to the domain need to be placed in the domain specific
# configuration file. This feature is disabled by default; set
# domain_specific_drivers_enabled to True to enable.
# domain_specific_drivers_enabled = False
# domain_config_dir = /etc/keystone/domains
# Maximum supported length for user passwords; decrease to improve performance.
# max_password_length = 4096

View File

@ -94,6 +94,7 @@ class UserAuthInfo(object):
self._assert_user_is_enabled(user_ref)
self.user_ref = user_ref
self.user_id = user_ref['id']
self.domain_id = domain_ref['id']
class Password(auth.AuthMethodHandler):
@ -106,7 +107,8 @@ class Password(auth.AuthMethodHandler):
try:
self.identity_api.authenticate(
user_id=user_info.user_id,
password=user_info.password)
password=user_info.password,
domain_scope=user_info.domain_id)
except AssertionError:
# authentication failed because of invalid username or password
msg = _('Invalid username or password')

View File

@ -25,9 +25,6 @@ from keystone.openstack.common import log as logging
LOG = logging.getLogger(__name__)
CONF = config.CONF
config.register_str('template_file',
default='default_catalog.templates',
group='catalog')
def parse_templates(template_lines):

View File

@ -24,6 +24,218 @@ _DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
_DEFAULT_AUTH_METHODS = ['external', 'password', 'token']
FILE_OPTIONS = {
'': [
cfg.StrOpt('admin_token', secret=True, default='ADMIN'),
cfg.StrOpt('bind_host', default='0.0.0.0'),
cfg.IntOpt('compute_port', default=8774),
cfg.IntOpt('admin_port', default=35357),
cfg.IntOpt('public_port', default=5000),
cfg.StrOpt('public_endpoint',
default='http://localhost:%(public_port)s/'),
cfg.StrOpt('admin_endpoint',
default='http://localhost:%(admin_port)s/'),
cfg.StrOpt('onready'),
cfg.StrOpt('auth_admin_prefix', default=''),
cfg.StrOpt('policy_file', default='policy.json'),
cfg.StrOpt('policy_default_rule', default=None),
# default max request size is 112k
cfg.IntOpt('max_request_body_size', default=114688),
cfg.IntOpt('max_param_size', default=64),
# we allow tokens to be a bit larger to accommodate PKI
cfg.IntOpt('max_token_size', default=8192),
cfg.StrOpt('member_role_id',
default='9fe2ff9ee4384b1894a90878d3e92bab'),
cfg.StrOpt('member_role_name', default='_member_'),
cfg.IntOpt('crypt_strength', default=40000)],
'identity': [
cfg.StrOpt('default_domain_id', default='default'),
cfg.BoolOpt('domain_specific_drivers_enabled',
default=False),
cfg.StrOpt('domain_config_dir',
default='/etc/keystone/domains'),
cfg.StrOpt('driver',
default=('keystone.identity.backends'
'.sql.Identity')),
cfg.IntOpt('max_password_length', default=4096)],
'trust': [
cfg.BoolOpt('enabled', default=True),
cfg.StrOpt('driver',
default='keystone.trust.backends.sql.Trust')],
'os_inherit': [
cfg.BoolOpt('enabled', default=False)],
'token': [
cfg.ListOpt('bind', default=[]),
cfg.StrOpt('enforce_token_bind', default='permissive'),
cfg.IntOpt('expiration', default=86400),
cfg.StrOpt('provider', default=None),
cfg.StrOpt('driver',
default='keystone.token.backends.sql.Token')],
'ssl': [
cfg.BoolOpt('enable', default=False),
cfg.StrOpt('certfile',
default="/etc/keystone/ssl/certs/keystone.pem"),
cfg.StrOpt('keyfile',
default="/etc/keystone/ssl/private/keystonekey.pem"),
cfg.StrOpt('ca_certs',
default="/etc/keystone/ssl/certs/ca.pem"),
cfg.StrOpt('ca_key',
default="/etc/keystone/ssl/certs/cakey.pem"),
cfg.BoolOpt('cert_required', default=False),
cfg.IntOpt('key_size', default=1024),
cfg.IntOpt('valid_days', default=3650),
cfg.StrOpt('ca_password', default=None),
cfg.StrOpt('cert_subject',
default='/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost')],
'signing': [
cfg.StrOpt('token_format', default=None),
cfg.StrOpt('certfile',
default="/etc/keystone/ssl/certs/signing_cert.pem"),
cfg.StrOpt('keyfile',
default="/etc/keystone/ssl/private/signing_key.pem"),
cfg.StrOpt('ca_certs',
default="/etc/keystone/ssl/certs/ca.pem"),
cfg.StrOpt('ca_key',
default="/etc/keystone/ssl/certs/cakey.pem"),
cfg.IntOpt('key_size', default=2048),
cfg.IntOpt('valid_days', default=3650),
cfg.StrOpt('ca_password', default=None),
cfg.StrOpt('cert_subject',
default=('/C=US/ST=Unset/L=Unset/O=Unset/'
'CN=www.example.com'))],
'sql': [
cfg.StrOpt('connection', secret=True,
default='sqlite:///keystone.db'),
cfg.IntOpt('idle_timeout', default=200)],
'assignment': [
# assignment has no default for backward compatibility reasons.
# If assignment driver is not specified, the identity driver chooses
# the backend
cfg.StrOpt('driver', default=None)],
'credential': [
cfg.StrOpt('driver',
default=('keystone.credential.backends'
'.sql.Credential'))],
'policy': [
cfg.StrOpt('driver',
default='keystone.policy.backends.sql.Policy')],
'ec2': [
cfg.StrOpt('driver',
default='keystone.contrib.ec2.backends.kvs.Ec2')],
'stats': [
cfg.StrOpt('driver',
default=('keystone.contrib.stats.backends'
'.kvs.Stats'))],
'ldap': [
cfg.StrOpt('url', default='ldap://localhost'),
cfg.StrOpt('user', default=None),
cfg.StrOpt('password', secret=True, default=None),
cfg.StrOpt('suffix', default='cn=example,cn=com'),
cfg.BoolOpt('use_dumb_member', default=False),
cfg.StrOpt('dumb_member', default='cn=dumb,dc=nonexistent'),
cfg.BoolOpt('allow_subtree_delete', default=False),
cfg.StrOpt('query_scope', default='one'),
cfg.IntOpt('page_size', default=0),
cfg.StrOpt('alias_dereferencing', default='default'),
cfg.StrOpt('user_tree_dn', default=None),
cfg.StrOpt('user_filter', default=None),
cfg.StrOpt('user_objectclass', default='inetOrgPerson'),
cfg.StrOpt('user_id_attribute', default='cn'),
cfg.StrOpt('user_name_attribute', default='sn'),
cfg.StrOpt('user_mail_attribute', default='email'),
cfg.StrOpt('user_pass_attribute', default='userPassword'),
cfg.StrOpt('user_enabled_attribute', default='enabled'),
cfg.StrOpt('user_domain_id_attribute',
default='businessCategory'),
cfg.IntOpt('user_enabled_mask', default=0),
cfg.StrOpt('user_enabled_default', default='True'),
cfg.ListOpt('user_attribute_ignore',
default='tenant_id,tenants'),
cfg.BoolOpt('user_allow_create', default=True),
cfg.BoolOpt('user_allow_update', default=True),
cfg.BoolOpt('user_allow_delete', default=True),
cfg.BoolOpt('user_enabled_emulation', default=False),
cfg.StrOpt('user_enabled_emulation_dn', default=None),
cfg.ListOpt('user_additional_attribute_mapping',
default=None),
cfg.StrOpt('tenant_tree_dn', default=None),
cfg.StrOpt('tenant_filter', default=None),
cfg.StrOpt('tenant_objectclass', default='groupOfNames'),
cfg.StrOpt('tenant_id_attribute', default='cn'),
cfg.StrOpt('tenant_member_attribute', default='member'),
cfg.StrOpt('tenant_name_attribute', default='ou'),
cfg.StrOpt('tenant_desc_attribute', default='description'),
cfg.StrOpt('tenant_enabled_attribute', default='enabled'),
cfg.StrOpt('tenant_domain_id_attribute',
default='businessCategory'),
cfg.ListOpt('tenant_attribute_ignore', default=''),
cfg.BoolOpt('tenant_allow_create', default=True),
cfg.BoolOpt('tenant_allow_update', default=True),
cfg.BoolOpt('tenant_allow_delete', default=True),
cfg.BoolOpt('tenant_enabled_emulation', default=False),
cfg.StrOpt('tenant_enabled_emulation_dn', default=None),
cfg.ListOpt('tenant_additional_attribute_mapping',
default=None),
cfg.StrOpt('role_tree_dn', default=None),
cfg.StrOpt('role_filter', default=None),
cfg.StrOpt('role_objectclass', default='organizationalRole'),
cfg.StrOpt('role_id_attribute', default='cn'),
cfg.StrOpt('role_name_attribute', default='ou'),
cfg.StrOpt('role_member_attribute', default='roleOccupant'),
cfg.ListOpt('role_attribute_ignore', default=''),
cfg.BoolOpt('role_allow_create', default=True),
cfg.BoolOpt('role_allow_update', default=True),
cfg.BoolOpt('role_allow_delete', default=True),
cfg.ListOpt('role_additional_attribute_mapping',
default=None),
cfg.StrOpt('group_tree_dn', default=None),
cfg.StrOpt('group_filter', default=None),
cfg.StrOpt('group_objectclass', default='groupOfNames'),
cfg.StrOpt('group_id_attribute', default='cn'),
cfg.StrOpt('group_name_attribute', default='ou'),
cfg.StrOpt('group_member_attribute', default='member'),
cfg.StrOpt('group_desc_attribute', default='description'),
cfg.StrOpt('group_domain_id_attribute',
default='businessCategory'),
cfg.ListOpt('group_attribute_ignore', default=''),
cfg.BoolOpt('group_allow_create', default=True),
cfg.BoolOpt('group_allow_update', default=True),
cfg.BoolOpt('group_allow_delete', default=True),
cfg.ListOpt('group_additional_attribute_mapping',
default=None),
cfg.StrOpt('tls_cacertfile', default=None),
cfg.StrOpt('tls_cacertdir', default=None),
cfg.BoolOpt('use_tls', default=False),
cfg.StrOpt('tls_req_cert', default='demand')],
'pam': [
cfg.StrOpt('userid', default=None),
cfg.StrOpt('password', default=None)],
'auth': [
cfg.ListOpt('methods', default=_DEFAULT_AUTH_METHODS),
cfg.StrOpt('password',
default='keystone.auth.plugins.token.Token'),
cfg.StrOpt('token',
default='keystone.auth.plugins.password.Password'),
#deals with REMOTE_USER authentication
cfg.StrOpt('external',
default='keystone.auth.plugins.external.ExternalDefault')],
'paste_deploy': [
cfg.StrOpt('config_file', default=None)],
'memcache': [
cfg.StrOpt('servers', default='localhost:11211'),
cfg.IntOpt('max_compare_and_set_retry', default=16)],
'catalog': [
cfg.StrOpt('template_file',
default='default_catalog.templates'),
cfg.StrOpt('driver',
default='keystone.catalog.backends.sql.Catalog')]}
CONF = cfg.CONF
@ -40,297 +252,35 @@ def setup_logging(conf, product_name='keystone'):
logging.setup(product_name)
def setup_authentication():
def setup_authentication(conf=None):
# register any non-default auth methods here (used by extensions, etc)
for method_name in CONF.auth.methods:
if conf is None:
conf = CONF
for method_name in conf.auth.methods:
if method_name not in _DEFAULT_AUTH_METHODS:
register_str(method_name, group="auth")
conf.register_opt(cfg.StrOpt(method_name), group='auth')
def register_str(*args, **kw):
conf = kw.pop('conf', CONF)
group = kw.pop('group', None)
return conf.register_opt(cfg.StrOpt(*args, **kw), group=group)
def configure(conf=None):
if conf is None:
conf = CONF
conf.register_cli_opt(
cfg.BoolOpt('standard-threads', default=False,
help='Do not monkey-patch threading system modules.'))
conf.register_cli_opt(
cfg.StrOpt('pydev-debug-host', default=None,
help='Host to connect to for remote debugger.'))
conf.register_cli_opt(
cfg.IntOpt('pydev-debug-port', default=None,
help='Port to connect to for remote debugger.'))
def register_cli_str(*args, **kw):
conf = kw.pop('conf', CONF)
group = kw.pop('group', None)
return conf.register_cli_opt(cfg.StrOpt(*args, **kw), group=group)
for section in FILE_OPTIONS:
for option in FILE_OPTIONS[section]:
if section:
conf.register_opt(option, group=section)
else:
conf.register_opt(option)
def register_list(*args, **kw):
conf = kw.pop('conf', CONF)
group = kw.pop('group', None)
return conf.register_opt(cfg.ListOpt(*args, **kw), group=group)
def register_cli_list(*args, **kw):
conf = kw.pop('conf', CONF)
group = kw.pop('group', None)
return conf.register_cli_opt(cfg.ListOpt(*args, **kw), group=group)
def register_bool(*args, **kw):
conf = kw.pop('conf', CONF)
group = kw.pop('group', None)
return conf.register_opt(cfg.BoolOpt(*args, **kw), group=group)
def register_cli_bool(*args, **kw):
conf = kw.pop('conf', CONF)
group = kw.pop('group', None)
return conf.register_cli_opt(cfg.BoolOpt(*args, **kw), group=group)
def register_int(*args, **kw):
conf = kw.pop('conf', CONF)
group = kw.pop('group', None)
return conf.register_opt(cfg.IntOpt(*args, **kw), group=group)
def register_cli_int(*args, **kw):
conf = kw.pop('conf', CONF)
group = kw.pop('group', None)
return conf.register_cli_opt(cfg.IntOpt(*args, **kw), group=group)
def configure():
register_cli_bool('standard-threads', default=False,
help='Do not monkey-patch threading system modules.')
register_cli_str('pydev-debug-host', default=None,
help='Host to connect to for remote debugger.')
register_cli_int('pydev-debug-port', default=None,
help='Port to connect to for remote debugger.')
register_str('admin_token', secret=True, default='ADMIN')
register_str('bind_host', default='0.0.0.0')
register_int('compute_port', default=8774)
register_int('admin_port', default=35357)
register_int('public_port', default=5000)
register_str(
'public_endpoint', default='http://localhost:%(public_port)s/')
register_str('admin_endpoint', default='http://localhost:%(admin_port)s/')
register_str('onready')
register_str('auth_admin_prefix', default='')
register_str('policy_file', default='policy.json')
register_str('policy_default_rule', default=None)
# default max request size is 112k
register_int('max_request_body_size', default=114688)
register_int('max_param_size', default=64)
# we allow tokens to be a bit larger to accommodate PKI
register_int('max_token_size', default=8192)
register_str(
'member_role_id', default='9fe2ff9ee4384b1894a90878d3e92bab')
register_str('member_role_name', default='_member_')
# identity
register_str('default_domain_id', group='identity', default='default')
register_int('max_password_length', group='identity', default=4096)
# trust
register_bool('enabled', group='trust', default=True)
# os_inherit
register_bool('enabled', group='os_inherit', default=False)
# binding
register_list('bind', group='token', default=[])
register_str('enforce_token_bind', group='token', default='permissive')
# ssl
register_bool('enable', group='ssl', default=False)
register_str('certfile', group='ssl',
default="/etc/keystone/ssl/certs/keystone.pem")
register_str('keyfile', group='ssl',
default="/etc/keystone/ssl/private/keystonekey.pem")
register_str('ca_certs', group='ssl',
default="/etc/keystone/ssl/certs/ca.pem")
register_str('ca_key', group='ssl',
default="/etc/keystone/ssl/certs/cakey.pem")
register_bool('cert_required', group='ssl', default=False)
register_int('key_size', group='ssl', default=1024)
register_int('valid_days', group='ssl', default=3650)
register_str('ca_password', group='ssl', default=None)
register_str('cert_subject', group='ssl',
default='/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost')
# signing
register_str(
'token_format', group='signing', default=None)
register_str(
'certfile',
group='signing',
default="/etc/keystone/ssl/certs/signing_cert.pem")
register_str(
'keyfile',
group='signing',
default="/etc/keystone/ssl/private/signing_key.pem")
register_str(
'ca_certs',
group='signing',
default="/etc/keystone/ssl/certs/ca.pem")
register_str('ca_key', group='signing',
default="/etc/keystone/ssl/certs/cakey.pem")
register_int('key_size', group='signing', default=2048)
register_int('valid_days', group='signing', default=3650)
register_str('ca_password', group='signing', default=None)
register_str('cert_subject', group='signing',
default='/C=US/ST=Unset/L=Unset/O=Unset/CN=www.example.com')
# sql
register_str('connection', group='sql', secret=True,
default='sqlite:///keystone.db')
register_int('idle_timeout', group='sql', default=200)
#assignment has no default for backward compatibility reasons.
#If assignment is not specified, the identity driver chooses the backend
register_str(
'driver',
group='assignment',
default=None)
register_str(
'driver',
group='catalog',
default='keystone.catalog.backends.sql.Catalog')
register_str(
'driver',
group='identity',
default='keystone.identity.backends.sql.Identity')
register_str(
'driver',
group='credential',
default='keystone.credential.backends.sql.Credential')
register_str(
'driver',
group='policy',
default='keystone.policy.backends.sql.Policy')
register_str(
'driver', group='token', default='keystone.token.backends.sql.Token')
register_str(
'driver', group='trust', default='keystone.trust.backends.sql.Trust')
register_str(
'driver', group='ec2', default='keystone.contrib.ec2.backends.kvs.Ec2')
register_str(
'driver',
group='stats',
default='keystone.contrib.stats.backends.kvs.Stats')
# ldap
register_str('url', group='ldap', default='ldap://localhost')
register_str('user', group='ldap', default=None)
register_str('password', group='ldap', secret=True, default=None)
register_str('suffix', group='ldap', default='cn=example,cn=com')
register_bool('use_dumb_member', group='ldap', default=False)
register_str('dumb_member', group='ldap', default='cn=dumb,dc=nonexistent')
register_bool('allow_subtree_delete', group='ldap', default=False)
register_str('query_scope', group='ldap', default='one')
register_int('page_size', group='ldap', default=0)
register_str('alias_dereferencing', group='ldap', default='default')
register_str('user_tree_dn', group='ldap', default=None)
register_str('user_filter', group='ldap', default=None)
register_str('user_objectclass', group='ldap', default='inetOrgPerson')
register_str('user_id_attribute', group='ldap', default='cn')
register_str('user_name_attribute', group='ldap', default='sn')
register_str('user_mail_attribute', group='ldap', default='email')
register_str('user_pass_attribute', group='ldap', default='userPassword')
register_str('user_enabled_attribute', group='ldap', default='enabled')
register_str(
'user_domain_id_attribute', group='ldap', default='businessCategory')
register_int('user_enabled_mask', group='ldap', default=0)
register_str('user_enabled_default', group='ldap', default='True')
register_list(
'user_attribute_ignore', group='ldap', default='tenant_id,tenants')
register_bool('user_allow_create', group='ldap', default=True)
register_bool('user_allow_update', group='ldap', default=True)
register_bool('user_allow_delete', group='ldap', default=True)
register_bool('user_enabled_emulation', group='ldap', default=False)
register_str('user_enabled_emulation_dn', group='ldap', default=None)
register_list(
'user_additional_attribute_mapping', group='ldap', default=None)
register_str('tenant_tree_dn', group='ldap', default=None)
register_str('tenant_filter', group='ldap', default=None)
register_str('tenant_objectclass', group='ldap', default='groupOfNames')
register_str('tenant_id_attribute', group='ldap', default='cn')
register_str('tenant_member_attribute', group='ldap', default='member')
register_str('tenant_name_attribute', group='ldap', default='ou')
register_str('tenant_desc_attribute', group='ldap', default='description')
register_str('tenant_enabled_attribute', group='ldap', default='enabled')
register_str(
'tenant_domain_id_attribute', group='ldap', default='businessCategory')
register_list('tenant_attribute_ignore', group='ldap', default='')
register_bool('tenant_allow_create', group='ldap', default=True)
register_bool('tenant_allow_update', group='ldap', default=True)
register_bool('tenant_allow_delete', group='ldap', default=True)
register_bool('tenant_enabled_emulation', group='ldap', default=False)
register_str('tenant_enabled_emulation_dn', group='ldap', default=None)
register_list(
'tenant_additional_attribute_mapping', group='ldap', default=None)
register_str('role_tree_dn', group='ldap', default=None)
register_str('role_filter', group='ldap', default=None)
register_str(
'role_objectclass', group='ldap', default='organizationalRole')
register_str('role_id_attribute', group='ldap', default='cn')
register_str('role_name_attribute', group='ldap', default='ou')
register_str('role_member_attribute', group='ldap', default='roleOccupant')
register_list('role_attribute_ignore', group='ldap', default='')
register_bool('role_allow_create', group='ldap', default=True)
register_bool('role_allow_update', group='ldap', default=True)
register_bool('role_allow_delete', group='ldap', default=True)
register_list(
'role_additional_attribute_mapping', group='ldap', default=None)
register_str('group_tree_dn', group='ldap', default=None)
register_str('group_filter', group='ldap', default=None)
register_str('group_objectclass', group='ldap', default='groupOfNames')
register_str('group_id_attribute', group='ldap', default='cn')
register_str('group_name_attribute', group='ldap', default='ou')
register_str('group_member_attribute', group='ldap', default='member')
register_str('group_desc_attribute', group='ldap', default='description')
register_str(
'group_domain_id_attribute', group='ldap', default='businessCategory')
register_list('group_attribute_ignore', group='ldap', default='')
register_bool('group_allow_create', group='ldap', default=True)
register_bool('group_allow_update', group='ldap', default=True)
register_bool('group_allow_delete', group='ldap', default=True)
register_list(
'group_additional_attribute_mapping', group='ldap', default=None)
register_str('tls_cacertfile', group='ldap', default=None)
register_str('tls_cacertdir', group='ldap', default=None)
register_bool('use_tls', group='ldap', default=False)
register_str('tls_req_cert', group='ldap', default='demand')
# pam
register_str('userid', group='pam', default=None)
register_str('password', group='pam', default=None)
# default authentication methods
register_list('methods', group='auth', default=_DEFAULT_AUTH_METHODS)
register_str(
'password', group='auth', default='keystone.auth.plugins.token.Token')
register_str(
'token', group='auth',
default='keystone.auth.plugins.password.Password')
#deals with REMOTE_USER authentication
register_str(
'external',
group='auth',
default='keystone.auth.plugins.external.ExternalDefault')
# register any non-default auth methods here (used by extensions, etc)
for method_name in CONF.auth.methods:
if method_name not in _DEFAULT_AUTH_METHODS:
register_str(method_name, group='auth')
# PasteDeploy config file
register_str('config_file', group='paste_deploy', default=None)
# token provider
register_str(
'provider',
group='token',
default=None)
setup_authentication(conf)

View File

@ -303,34 +303,35 @@ class V3Controller(V2Controller):
ref['id'] = uuid.uuid4().hex
return ref
def _get_domain_id_for_request(self, context):
"""Get the domain_id for a v3 call."""
if context['is_admin']:
return DEFAULT_DOMAIN_ID
# Fish the domain_id out of the token
#
# We could make this more efficient by loading the domain_id
# into the context in the wrapper function above (since
# this version of normalize_domain will only be called inside
# a v3 protected call). However, this optimization is probably not
# worth the duplication of state
try:
token_ref = self.token_api.get_token(
token_id=context['token_id'])
except exception.TokenNotFound:
LOG.warning(_('Invalid token in _get_domain_id_for_request'))
raise exception.Unauthorized()
if 'domain' in token_ref:
return token_ref['domain']['id']
else:
return DEFAULT_DOMAIN_ID
def _normalize_domain_id(self, context, ref):
"""Fill in domain_id if not specified in a v3 call."""
if 'domain_id' not in ref:
if context['is_admin']:
ref['domain_id'] = DEFAULT_DOMAIN_ID
else:
# Fish the domain_id out of the token
#
# We could make this more efficient by loading the domain_id
# into the context in the wrapper function above (since
# this version of normalize_domain will only be called inside
# a v3 protected call). However, given that we only use this
# for creating entities, this optimization is probably not
# worth the duplication of state
try:
token_ref = self.token_api.get_token(
token_id=context['token_id'])
except exception.TokenNotFound:
LOG.warning(_('Invalid token in normalize_domain_id'))
raise exception.Unauthorized()
if 'domain' in token_ref:
ref['domain_id'] = token_ref['domain']['id']
else:
# FIXME(henry-nash) Revisit this once v3 token scoping
# across domains has been hashed out
ref['domain_id'] = DEFAULT_DOMAIN_ID
ref['domain_id'] = self._get_domain_id_for_request(context)
return ref
def _filter_domain_id(self, ref):

View File

@ -123,18 +123,14 @@ server_fail = False
class FakeShelve(dict):
@classmethod
def get_instance(cls):
try:
return cls.__instance
except AttributeError:
cls.__instance = cls()
return cls.__instance
def sync(self):
pass
FakeShelves = {}
class FakeLdap(object):
"""Fake LDAP connection."""
@ -142,8 +138,10 @@ class FakeLdap(object):
def __init__(self, url):
LOG.debug(_('FakeLdap initialize url=%s'), url)
if url == 'fake://memory':
self.db = FakeShelve.get_instance()
if url.startswith('fake://memory'):
if url not in FakeShelves:
FakeShelves[url] = FakeShelve()
self.db = FakeShelves[url]
else:
self.db = shelve.open(url[7:])

View File

@ -32,7 +32,6 @@ from keystone.openstack.common import log as logging
CONF = config.CONF
config.register_int('crypt_strength', default=40000)
LOG = logging.getLogger(__name__)

View File

@ -25,15 +25,8 @@ config.configure()
CONF = config.CONF
setup_logging = config.setup_logging
register_str = config.register_str
register_cli_str = config.register_cli_str
register_list = config.register_list
register_cli_list = config.register_cli_list
register_bool = config.register_bool
register_cli_bool = config.register_cli_bool
register_int = config.register_int
register_cli_int = config.register_cli_int
setup_authentication = config.setup_authentication
configure = config.configure
def find_paste_config():

View File

@ -27,6 +27,9 @@ class Identity(kvs.Base, identity.Driver):
def default_assignment_driver(self):
return "keystone.assignment.backends.kvs.Assignment"
def is_domain_aware(self):
return True
# Public interface
def authenticate(self, user_id, password):
user_ref = None

View File

@ -41,14 +41,19 @@ DEFAULT_DOMAIN = {
@dependency.requires('assignment_api')
class Identity(identity.Driver):
def __init__(self):
def __init__(self, conf=None):
super(Identity, self).__init__()
self.user = UserApi(CONF)
self.group = GroupApi(CONF)
if conf is None:
conf = CONF
self.user = UserApi(conf)
self.group = GroupApi(conf)
def default_assignment_driver(self):
return "keystone.assignment.backends.ldap.Assignment"
def is_domain_aware(self):
return False
# Identity interface
def create_project(self, project_id, project):
@ -68,37 +73,31 @@ class Identity(identity.Driver):
raise AssertionError('Invalid user / password')
except Exception:
raise AssertionError('Invalid user / password')
return self.assignment_api._set_default_domain(
identity.filter_user(user_ref))
return identity.filter_user(user_ref)
def _get_user(self, user_id):
return self.user.get(user_id)
def get_user(self, user_id):
ref = identity.filter_user(self._get_user(user_id))
return self.assignment_api._set_default_domain(ref)
return identity.filter_user(self._get_user(user_id))
def list_users(self):
return (self.assignment_api._set_default_domain
(self.user.get_all_filtered()))
return self.user.get_all_filtered()
def get_user_by_name(self, user_name, domain_id):
self.assignment_api._validate_default_domain_id(domain_id)
ref = identity.filter_user(self.user.get_by_name(user_name))
return self.assignment_api._set_default_domain(ref)
# domain_id will already have been handled in the Manager layer,
# parameter left in so this matches the Driver specification
return identity.filter_user(self.user.get_by_name(user_name))
# CRUD
def create_user(self, user_id, user):
user = self.assignment_api._validate_default_domain(user)
user_ref = self.user.create(user)
tenant_id = user.get('tenant_id')
if tenant_id is not None:
self.assignment_api.add_user_to_project(tenant_id, user_id)
return (self.assignment_api._set_default_domain
(identity.filter_user(user_ref)))
return identity.filter_user(user_ref)
def update_user(self, user_id, user):
user = self.assignment_api._validate_default_domain(user)
if 'id' in user and user['id'] != user_id:
raise exception.ValidationError('Cannot change user ID')
old_obj = self.user.get(user_id)
@ -121,8 +120,7 @@ class Identity(identity.Driver):
user['enabled_nomask'] = old_obj['enabled_nomask']
self.user.mask_enabled_attribute(user)
self.user.update(user_id, user, old_obj)
return (self.assignment_api._set_default_domain
(self.user.get_filtered(user_id)))
return self.user.get_filtered(user_id)
def delete_user(self, user_id):
self.assignment_api.delete_user(user_id)
@ -138,21 +136,16 @@ class Identity(identity.Driver):
self.user.delete(user_id)
def create_group(self, group_id, group):
group = self.assignment_api._validate_default_domain(group)
group['name'] = clean.group_name(group['name'])
return self.assignment_api._set_default_domain(
self.group.create(group))
return self.group.create(group)
def get_group(self, group_id):
return self.assignment_api._set_default_domain(
self.group.get(group_id))
return self.group.get(group_id)
def update_group(self, group_id, group):
group = self.assignment_api._validate_default_domain(group)
if 'name' in group:
group['name'] = clean.group_name(group['name'])
return (self.assignment_api._set_default_domain
(self.group.update(group_id, group)))
return self.group.update(group_id, group)
def delete_group(self, group_id):
return self.group.delete(group_id)
@ -172,11 +165,10 @@ class Identity(identity.Driver):
def list_groups_for_user(self, user_id):
self.get_user(user_id)
user_dn = self.user._id_to_dn(user_id)
return (self.assignment_api._set_default_domain
(self.group.list_user_groups(user_dn)))
return self.group.list_user_groups(user_dn)
def list_groups(self):
return self.assignment_api._set_default_domain(self.group.get_all())
return self.group.get_all()
def list_users_in_group(self, group_id):
self.get_group(group_id)
@ -190,7 +182,7 @@ class Identity(identity.Driver):
" '%(group_id)s'. The user should be removed"
" from the group. The user will be ignored.") %
dict(user_dn=user_dn, group_id=group_id))
return self.assignment_api._set_default_domain(users)
return users
def check_user_in_group(self, user_id, group_id):
self.get_user(user_id)

View File

@ -58,6 +58,9 @@ class PamIdentity(identity.Driver):
Tenant is always the same as User, root user has admin role.
"""
def is_domain_aware(self):
return False
def authenticate(self, user_id, password):
auth = pam.authenticate if pam else PAM_authenticate
if not auth(user_id, password):

View File

@ -85,6 +85,9 @@ class Identity(sql.Base, identity.Driver):
"""
return utils.check_password(password, user_ref.password)
def is_domain_aware(self):
return True
# Identity interface
def authenticate(self, user_id, password):
session = self.get_session()

View File

@ -620,23 +620,30 @@ class UserV3(controller.V3Controller):
@controller.filterprotected('domain_id', 'email', 'enabled', 'name')
def list_users(self, context, filters):
refs = self.identity_api.list_users()
refs = self.identity_api.list_users(
domain_scope=self._get_domain_id_for_request(context))
return UserV3.wrap_collection(context, refs, filters)
@controller.filterprotected('domain_id', 'email', 'enabled', 'name')
def list_users_in_group(self, context, filters, group_id):
refs = self.identity_api.list_users_in_group(group_id)
refs = self.identity_api.list_users_in_group(
group_id,
domain_scope=self._get_domain_id_for_request(context))
return UserV3.wrap_collection(context, refs, filters)
@controller.protected
def get_user(self, context, user_id):
ref = self.identity_api.get_user(user_id)
ref = self.identity_api.get_user(
user_id,
domain_scope=self._get_domain_id_for_request(context))
return UserV3.wrap_member(context, ref)
@controller.protected
def update_user(self, context, user_id, user):
self._require_matching_id(user_id, user)
ref = self.identity_api.update_user(user_id, user)
ref = self.identity_api.update_user(
user_id, user,
domain_scope=self._get_domain_id_for_request(context))
if user.get('password') or not user.get('enabled', True):
# revoke all tokens owned by this user
@ -646,18 +653,24 @@ class UserV3(controller.V3Controller):
@controller.protected
def add_user_to_group(self, context, user_id, group_id):
self.identity_api.add_user_to_group(user_id, group_id)
self.identity_api.add_user_to_group(
user_id, group_id,
domain_scope=self._get_domain_id_for_request(context))
# Delete any tokens so that group membership can have an
# immediate effect
self._delete_tokens_for_user(user_id)
@controller.protected
def check_user_in_group(self, context, user_id, group_id):
return self.identity_api.check_user_in_group(user_id, group_id)
return self.identity_api.check_user_in_group(
user_id, group_id,
domain_scope=self._get_domain_id_for_request(context))
@controller.protected
def remove_user_from_group(self, context, user_id, group_id):
self.identity_api.remove_user_from_group(user_id, group_id)
self.identity_api.remove_user_from_group(
user_id, group_id,
domain_scope=self._get_domain_id_for_request(context))
self._delete_tokens_for_user(user_id)
def _delete_user(self, context, user_id):
@ -667,11 +680,13 @@ class UserV3(controller.V3Controller):
self.credential_api.delete_credential(cred['id'])
# Make sure any tokens are marked as deleted
domain_id = self._get_domain_id_for_request(context)
self._delete_tokens_for_user(user_id)
# Finally delete the user itself - the backend is
# responsible for deleting any role assignments related
# to this user
return self.identity_api.delete_user(user_id)
return self.identity_api.delete_user(
user_id, domain_scope=domain_id)
@controller.protected
def delete_user(self, context, user_id):
@ -693,24 +708,31 @@ class GroupV3(controller.V3Controller):
@controller.filterprotected('domain_id', 'name')
def list_groups(self, context, filters):
refs = self.identity_api.list_groups()
refs = self.identity_api.list_groups(
domain_scope=self._get_domain_id_for_request(context))
return GroupV3.wrap_collection(context, refs, filters)
@controller.filterprotected('name')
def list_groups_for_user(self, context, filters, user_id):
refs = self.identity_api.list_groups_for_user(user_id)
refs = self.identity_api.list_groups_for_user(
user_id,
domain_scope=self._get_domain_id_for_request(context))
return GroupV3.wrap_collection(context, refs, filters)
@controller.protected
def get_group(self, context, group_id):
ref = self.identity_api.get_group(group_id)
ref = self.identity_api.get_group(
group_id,
domain_scope=self._get_domain_id_for_request(context))
return GroupV3.wrap_member(context, ref)
@controller.protected
def update_group(self, context, group_id, group):
self._require_matching_id(group_id, group)
ref = self.identity_api.update_group(group_id, group)
ref = self.identity_api.update_group(
group_id, group,
domain_scope=self._get_domain_id_for_request(context))
return GroupV3.wrap_member(context, ref)
def _delete_group(self, context, group_id):
@ -720,8 +742,10 @@ class GroupV3(controller.V3Controller):
# deletion, so that we can remove these tokens after we know
# the group deletion succeeded.
user_refs = self.identity_api.list_users_in_group(group_id)
self.identity_api.delete_group(group_id)
domain_id = self._get_domain_id_for_request(context)
user_refs = self.identity_api.list_users_in_group(
group_id, domain_scope=domain_id)
self.identity_api.delete_group(group_id, domain_scope=domain_id)
for user in user_refs:
self._delete_tokens_for_user(user['id'])

View File

@ -16,11 +16,17 @@
"""Main entry point into the Identity service."""
import functools
import os
from oslo.config import cfg
from keystone import clean
from keystone.common import dependency
from keystone.common import manager
from keystone import config
from keystone import exception
from keystone.openstack.common import importutils
from keystone.openstack.common import log as logging
@ -51,6 +57,121 @@ def filter_user(user_ref):
return user_ref
class DomainConfigs(dict):
"""Discover, store and provide access to domain specifc configs.
The setup_domain_drives() call will be made via the wrapper from
the first call to any driver function handled by this manager. This
setup call it will scan the domain config directory for files of the form
keystone.<domain_name>.conf
For each file, the domain_name will be turned into a domain_id and then
this class will:
- Create a new config structure, adding in the specific additional options
defined in this config file
- Initialise a new instance of the required driver with this new config.
"""
configured = False
driver = None
def _load_driver(self, assignment_api, domain_id):
domain_config = self[domain_id]
domain_config['driver'] = (
importutils.import_object(
domain_config['cfg'].identity.driver, domain_config['cfg']))
domain_config['driver'].assignment_api = assignment_api
def _load_config(self, assignment_api, file_list, domain_name):
try:
domain_ref = assignment_api.get_domain_by_name(domain_name)
except exception.DomainNotFound:
msg = (_('Invalid domain name (%s) found in config file name')
% domain_name)
LOG.warning(msg)
if domain_ref:
# Create a new entry in the domain config dict, which contains
# a new instance of both the conf environment and driver using
# options defined in this set of config files. Later, when we
# service calls via this Manager, we'll index via this domain
# config dict to make sure we call the right driver
domain = domain_ref['id']
self[domain] = {}
self[domain]['cfg'] = cfg.ConfigOpts()
config.configure(conf=self[domain]['cfg'])
self[domain]['cfg'](args=[], project='keystone',
default_config_files=file_list)
self._load_driver(assignment_api, domain)
def setup_domain_drivers(self, standard_driver, assignment_api):
# This is called by the api call wrapper
self.configured = True
self.driver = standard_driver
conf_dir = CONF.identity.domain_config_dir
if not os.path.exists(conf_dir):
msg = _('Unable to locate domain config directory: %s') % conf_dir
LOG.warning(msg)
return
for r, d, f in os.walk(conf_dir):
for file in f:
if file.startswith('keystone.') and file.endswith('.conf'):
names = file.split('.')
if len(names) == 3:
self._load_config(assignment_api,
[os.path.join(r, file)],
names[1])
else:
msg = (_('Ignoring file (%s) while scanning domain '
'config directory') % file)
LOG.debug(msg)
def get_domain_driver(self, domain_id):
if domain_id in self:
return self[domain_id]['driver']
def get_domain_conf(self, domain_id):
if domain_id in self:
return self[domain_id]['cfg']
def reload_domain_driver(self, assignment_api, domain_id):
# Only used to support unit tests that want to set
# new config values. This should only be called once
# the domains have been configured, since it relies on
# the fact that the configuration files have already been
# read.
if self.configured:
if domain_id in self:
self._load_driver(assignment_api, domain_id)
else:
# The standard driver
self.driver = self.driver()
self.driver.assignment_api = assignment_api
def domains_configured(f):
"""Wraps API calls to lazy load domain configs after init.
This is required since the assignment manager needs to be initialized
before this manager, and yet this manager's init wants to be
able to make assignment calls (to build the domain configs). So
instead, we check if the domains have been initialized on entry
to each call, and if requires load them,
"""
@functools.wraps(f)
def wrapper(self, *args, **kwargs):
if (not self.domain_configs.configured and
CONF.identity.domain_specific_drivers_enabled):
self.domain_configs.setup_domain_drivers(
self.driver, self.assignment_api)
return f(self, *args, **kwargs)
return wrapper
@dependency.provider('identity_api')
@dependency.requires('assignment_api')
class Manager(manager.Manager):
@ -59,30 +180,228 @@ class Manager(manager.Manager):
See :mod:`keystone.common.manager.Manager` for more details on how this
dynamically calls the backend.
This class also handles the support of domain specific backends, by using
the DomainConfigs class. The setup call for DomainConfigs is called
from with the @domains_configured wrapper in a lazy loading fashion
to get around the fact that we can't satisfy the assignment api it needs
from within our __init__() function since the assignment driver is not
itself yet intitalized.
Each of the identity calls are pre-processed here to choose, based on
domain, which of the drivers should be called. The non-domain-specific
driver is still in place, and is used if there is no specific driver for
the domain in question.
"""
def __init__(self):
super(Manager, self).__init__(CONF.identity.driver)
self.domain_configs = DomainConfigs()
# Domain ID normalization methods
def _set_domain_id(self, ref, domain_id):
if isinstance(ref, dict):
ref = ref.copy()
ref['domain_id'] = domain_id
return ref
elif isinstance(ref, list):
return [self._set_domain_id(x, domain_id) for x in ref]
else:
raise ValueError(_('Expected dict or list: %s') % type(ref))
def _clear_domain_id(self, ref):
# Clear the domain_id, and then check to ensure that if this
# was not the default domain, it is being handled by its own
# backend driver.
ref = ref.copy()
domain_id = ref.pop('domain_id', CONF.identity.default_domain_id)
if (domain_id != CONF.identity.default_domain_id and
domain_id not in self.domain_configs):
raise exception.DomainNotFound(domain_id=domain_id)
return ref
def _normalize_scope(self, domain_scope):
if domain_scope is None:
return CONF.identity.default_domain_id
else:
return domain_scope
def _select_identity_driver(self, domain_id):
driver = self.domain_configs.get_domain_driver(domain_id)
if driver:
return driver
else:
return self.driver
def _get_domain_conf(self, domain_id):
conf = self.domain_configs.get_domain_conf(domain_id)
if conf:
return conf
else:
return CONF
def _get_domain_id_and_driver(self, domain_scope):
domain_id = self._normalize_scope(domain_scope)
driver = self._select_identity_driver(domain_id)
return (domain_id, driver)
# The actual driver calls - these are pre/post processed here as
# part of the Manager layer to make sure we:
#
# - select the right driver for this domain
# - clear/set domain_ids for drivers that do not support domains
@domains_configured
def authenticate(self, user_id, password, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
ref = driver.authenticate(user_id, password)
if not driver.is_domain_aware():
ref = self._set_domain_id(ref, domain_id)
return ref
@domains_configured
def create_user(self, user_id, user_ref):
user = user_ref.copy()
user['name'] = clean.user_name(user['name'])
user.setdefault('enabled', True)
user['enabled'] = clean.user_enabled(user['enabled'])
return self.driver.create_user(user_id, user)
def update_user(self, user_id, user_ref):
# For creating a user, the domain is in the object itself
domain_id = user_ref['domain_id']
driver = self._select_identity_driver(domain_id)
if not driver.is_domain_aware():
user = self._clear_domain_id(user)
ref = driver.create_user(user_id, user)
if not driver.is_domain_aware():
ref = self._set_domain_id(ref, domain_id)
return ref
@domains_configured
def get_user(self, user_id, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
ref = driver.get_user(user_id)
if not driver.is_domain_aware():
ref = self._set_domain_id(ref, domain_id)
return ref
@domains_configured
def get_user_by_name(self, user_name, domain_id):
driver = self._select_identity_driver(domain_id)
ref = driver.get_user_by_name(user_name, domain_id)
if not driver.is_domain_aware():
ref = self._set_domain_id(ref, domain_id)
return ref
@domains_configured
def list_users(self, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
user_list = driver.list_users()
if not driver.is_domain_aware():
user_list = self._set_domain_id(user_list, domain_id)
return user_list
@domains_configured
def update_user(self, user_id, user_ref, domain_scope=None):
user = user_ref.copy()
if 'name' in user:
user['name'] = clean.user_name(user['name'])
if 'enabled' in user:
user['enabled'] = clean.user_enabled(user['enabled'])
return self.driver.update_user(user_id, user)
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
if not driver.is_domain_aware():
user = self._clear_domain_id(user)
ref = driver.update_user(user_id, user)
if not driver.is_domain_aware():
ref = self._set_domain_id(ref, domain_id)
return ref
@domains_configured
def delete_user(self, user_id, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
driver.delete_user(user_id)
@domains_configured
def create_group(self, group_id, group_ref):
group = group_ref.copy()
group.setdefault('description', '')
return self.driver.create_group(group_id, group)
# For creating a group, the domain is in the object itself
domain_id = group_ref['domain_id']
driver = self._select_identity_driver(domain_id)
if not driver.is_domain_aware():
group = self._clear_domain_id(group)
ref = driver.create_group(group_id, group)
if not driver.is_domain_aware():
ref = self._set_domain_id(ref, domain_id)
return ref
@domains_configured
def get_group(self, group_id, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
ref = driver.get_group(group_id)
if not driver.is_domain_aware():
ref = self._set_domain_id(ref, domain_id)
return ref
@domains_configured
def update_group(self, group_id, group, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
if not driver.is_domain_aware():
group = self._clear_domain_id(group)
ref = driver.update_group(group_id, group)
if not driver.is_domain_aware():
ref = self._set_domain_id(ref, domain_id)
return ref
@domains_configured
def delete_group(self, group_id, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
driver.delete_group(group_id)
@domains_configured
def add_user_to_group(self, user_id, group_id, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
driver.add_user_to_group(user_id, group_id)
@domains_configured
def remove_user_from_group(self, user_id, group_id, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
driver.remove_user_from_group(user_id, group_id)
@domains_configured
def list_groups_for_user(self, user_id, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
group_list = driver.list_groups_for_user(user_id)
if not driver.is_domain_aware():
group_list = self._set_domain_id(group_list, domain_id)
return group_list
@domains_configured
def list_groups(self, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
group_list = driver.list_groups()
if not driver.is_domain_aware():
group_list = self._set_domain_id(group_list, domain_id)
return group_list
@domains_configured
def list_users_in_group(self, group_id, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
user_list = driver.list_users_in_group(group_id)
if not driver.is_domain_aware():
user_list = self._set_domain_id(user_list, domain_id)
return user_list
@domains_configured
def check_user_in_group(self, user_id, group_id, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
return driver.check_user_in_group(user_id, group_id)
# TODO(henry-nash, ayoung) The following cross calls to the assignment
# API should be removed, with the controller and tests making the correct
# calls direct to assignment.
def create_project(self, tenant_id, tenant_ref):
tenant = tenant_ref.copy()
@ -358,6 +677,8 @@ class Driver(object):
"""
raise exception.NotImplemented()
#end of identity
def is_domain_aware(self):
"""Indicates if Driver supports domains."""
raise exception.NotImplemented()
# Assignments
#end of identity

View File

@ -0,0 +1,35 @@
[sql]
connection = sqlite://
#For a file based sqlite use
#connection = sqlite:////tmp/keystone.db
#To Test MySQL:
#connection = mysql://keystone:keystone@localhost/keystone?charset=utf8
#To Test PostgreSQL:
#connection = postgresql://keystone:keystone@localhost/keystone?client_encoding=utf8
idle_timeout = 200
[identity]
# common identity backend is SQL, domain specific configs will
# set their backends to ldap
driver = keystone.identity.backends.sql.Identity
# The test setup will set this to True, to allow easier creation
# of initial domain data
# domain_specific_drivers_enabled = True
[assignment]
driver = keystone.assignment.backends.sql.Assignment
[token]
driver = keystone.token.backends.sql.Token
[ec2]
driver = keystone.contrib.ec2.backends.sql.Ec2
[catalog]
driver = keystone.catalog.backends.sql.Catalog
[policy]
driver = keystone.policy.backends.sql.Policy
[trust]
driver = keystone.trust.backends.sql.Trust

View File

@ -292,9 +292,11 @@ class TestCase(NoModule, unittest.TestCase):
for domain in fixtures.DOMAINS:
try:
rv = self.identity_api.create_domain(domain['id'], domain)
except (exception.Conflict, exception.NotImplemented):
pass
setattr(self, 'domain_%s' % domain['id'], domain)
except exception.Conflict:
rv = self.identity_api.get_domain(domain['id'])
except exception.NotImplemented:
rv = domain
setattr(self, 'domain_%s' % domain['id'], rv)
for tenant in fixtures.TENANTS:
try:

View File

@ -0,0 +1,14 @@
# The domain-specific configuration file for the default domain for
# use with unit tests.
#
# The domain_name of the default domain is 'Default', hence the
# strange mix of upper/lower case in the file name.
[ldap]
url = fake://memory
user = cn=Admin
password = password
suffix = cn=example,cn=com
[identity]
driver = keystone.identity.backends.ldap.Identity

View File

@ -0,0 +1,11 @@
# The domain-specific configuration file for the test domain
# 'domain1' for use with unit tests.
[ldap]
url = fake://memory1
user = cn=Admin
password = password
suffix = cn=example,cn=com
[identity]
driver = keystone.identity.backends.ldap.Identity

View File

@ -0,0 +1,13 @@
# The domain-specific configuration file for the test domain
# 'domain2' for use with unit tests.
[ldap]
url = fake://memory
user = cn=Admin
password = password
suffix = cn=myroot,cn=com
group_tree_dn = ou=UserGroups,dc=myroot,dc=org
user_tree_dn = ou=Users,dc=myroot,dc=org
[identity]
driver = keystone.identity.backends.ldap.Identity

View File

@ -105,7 +105,9 @@ class IdentityTests(object):
self.assertIn(CONF.member_role_id, role_list)
def test_password_hashed(self):
user_ref = self.identity_api._get_user(self.user_foo['id'])
driver = self.identity_api._select_identity_driver(
self.user_foo['domain_id'])
user_ref = driver._get_user(self.user_foo['id'])
self.assertNotEqual(user_ref['password'], self.user_foo['password'])
def test_create_unicode_user_name(self):
@ -1521,7 +1523,8 @@ class IdentityTests(object):
self.assertRaises(exception.UserNotFound,
self.identity_api.update_user,
user_id,
{'id': user_id})
{'id': user_id,
'domain_id': DEFAULT_DOMAIN_ID})
def test_delete_user_with_project_association(self):
user = {'id': uuid.uuid4().hex,

View File

@ -38,8 +38,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
return self.identity_api.get_domain(CONF.identity.default_domain_id)
def clear_database(self):
db = fakeldap.FakeShelve().get_instance()
db.clear()
for shelf in fakeldap.FakeShelves:
fakeldap.FakeShelves[shelf].clear()
def reload_backends(self, domain_id):
# Only one backend unless we are using separate domain backends
self.load_backends()
def get_config(self, domain_id):
# Only one conf structure unless we are using separate domain backends
return CONF
def _set_config(self):
self.config([test.etcdir('keystone.conf.sample'),
@ -57,6 +65,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
user = {'id': 'fake1',
'name': 'fake1',
'password': 'fakepass1',
'domain_id': CONF.identity.default_domain_id,
'tenants': ['bar']}
self.identity_api.create_user('fake1', user)
user_ref = self.identity_api.get_user('fake1')
@ -71,14 +80,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
'fake1')
def test_configurable_forbidden_user_actions(self):
CONF.ldap.user_allow_create = False
CONF.ldap.user_allow_update = False
CONF.ldap.user_allow_delete = False
self.load_backends()
conf = self.get_config(CONF.identity.default_domain_id)
conf.ldap.user_allow_create = False
conf.ldap.user_allow_update = False
conf.ldap.user_allow_delete = False
self.reload_backends(CONF.identity.default_domain_id)
user = {'id': 'fake1',
'name': 'fake1',
'password': 'fakepass1',
'domain_id': CONF.identity.default_domain_id,
'tenants': ['bar']}
self.assertRaises(exception.ForbiddenAction,
self.identity_api.create_user,
@ -100,8 +111,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
self.user_foo.pop('password')
self.assertDictEqual(user_ref, self.user_foo)
CONF.ldap.user_filter = '(CN=DOES_NOT_MATCH)'
self.load_backends()
conf = self.get_config(user_ref['domain_id'])
conf.ldap.user_filter = '(CN=DOES_NOT_MATCH)'
self.reload_backends(user_ref['domain_id'])
self.assertRaises(exception.UserNotFound,
self.identity_api.get_user,
self.user_foo['id'])
@ -205,18 +217,21 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
# Create a group
group_id = None
group = dict(name=uuid.uuid4().hex)
group = dict(name=uuid.uuid4().hex,
domain_id=CONF.identity.default_domain_id)
group_id = self.identity_api.create_group(group_id, group)['id']
# Create a couple of users and add them to the group.
user_id = None
user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex)
user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex,
domain_id=CONF.identity.default_domain_id)
user_1_id = self.identity_api.create_user(user_id, user)['id']
self.identity_api.add_user_to_group(user_1_id, group_id)
user_id = None
user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex)
user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex,
domain_id=CONF.identity.default_domain_id)
user_2_id = self.identity_api.create_user(user_id, user)['id']
self.identity_api.add_user_to_group(user_2_id, group_id)
@ -224,7 +239,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
# Delete user 2
# NOTE(blk-u): need to go directly to user interface to keep from
# updating the group.
self.identity_api.driver.user.delete(user_2_id)
driver = self.identity_api._select_identity_driver(
user['domain_id'])
driver.user.delete(user_2_id)
# List group users and verify only user 1.
res = self.identity_api.list_users_in_group(group_id)
@ -249,13 +266,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
self.identity_api.create_user(user['id'], user)
self.identity_api.add_user_to_project(self.tenant_baz['id'],
user['id'])
self.identity_api.driver.user.LDAP_USER = None
self.identity_api.driver.user.LDAP_PASSWORD = None
driver = self.identity_api._select_identity_driver(
user['domain_id'])
driver.user.LDAP_USER = None
driver.user.LDAP_PASSWORD = None
self.assertRaises(AssertionError,
self.identity_api.authenticate,
user_id=user['id'],
password=None)
password=None,
domain_scope=user['domain_id'])
# (spzala)The group and domain crud tests below override the standard ones
# in test_backend.py so that we can exclude the update name test, since we
@ -460,7 +480,8 @@ class LDAPIdentity(test.TestCase, BaseLDAPIdentity):
self.load_backends()
self.load_fixtures(default_fixtures)
user = {'id': 'fake1', 'name': 'fake1', 'enabled': True}
user = {'id': 'fake1', 'name': 'fake1', 'enabled': True,
'domain_id': CONF.identity.default_domain_id}
self.identity_api.create_user('fake1', user)
user_ref = self.identity_api.get_user('fake1')
self.assertEqual(user_ref['enabled'], True)
@ -512,6 +533,7 @@ class LDAPIdentity(test.TestCase, BaseLDAPIdentity):
'id': 'extra_attributes',
'name': 'EXTRA_ATTRIBUTES',
'password': 'extra',
'domain_id': CONF.identity.default_domain_id
}
self.identity_api.create_user(user['id'], user)
dn, attrs = self.identity_api.driver.user._ldap_get(user['id'])
@ -745,3 +767,230 @@ class LdapIdentitySqlAssignment(sql.Base, test.TestCase, BaseLDAPIdentity):
def test_role_filter(self):
self.skipTest(
'N/A: Not part of SQL backend')
class MultiLDAPandSQLIdentity(sql.Base, test.TestCase, BaseLDAPIdentity):
"""Class to test common SQL plus individual LDAP backends.
We define a set of domains and domain-specific backends:
- A separate LDAP backend for the default domain
- A separate LDAP backend for domain1
- domain2 shares the same LDAP as domain1, but uses a different
tree attach point
- An SQL backend for all other domains (which will include domain3
and domain4)
Normally one would expect that the default domain would be handled as
part of the "other domains" - however the above provides better
test coverage since most of the existing backend tests use the default
domain.
"""
def setUp(self):
super(MultiLDAPandSQLIdentity, self).setUp()
self._set_config()
self.load_backends()
self.engine = self.get_engine()
sql.ModelBase.metadata.create_all(bind=self.engine)
self._setup_domain_test_data()
# All initial domain data setup complete, time to switch on support
# for separate backends per domain.
self.orig_config_domains_enabled = (
config.CONF.identity.domain_specific_drivers_enabled)
self.opt_in_group('identity', domain_specific_drivers_enabled=True)
self.orig_config_dir = (
config.CONF.identity.domain_config_dir)
self.opt_in_group('identity', domain_config_dir=test.TESTSDIR)
self._set_domain_configs()
self.clear_database()
self.load_fixtures(default_fixtures)
def tearDown(self):
super(MultiLDAPandSQLIdentity, self).tearDown()
self.opt_in_group(
'identity',
domain_config_dir=self.orig_config_dir)
self.opt_in_group(
'identity',
domain_specific_drivers_enabled=self.orig_config_domains_enabled)
sql.ModelBase.metadata.drop_all(bind=self.engine)
self.engine.dispose()
sql.set_global_engine(None)
def _set_config(self):
self.config([test.etcdir('keystone.conf.sample'),
test.testsdir('test_overrides.conf'),
test.testsdir('backend_multi_ldap_sql.conf')])
def _setup_domain_test_data(self):
def create_domain(domain):
try:
ref = self.assignment_api.create_domain(
domain['id'], domain)
except exception.Conflict:
ref = (
self.assignment_api.get_domain_by_name(domain['name']))
return ref
self.domain_default = create_domain(assignment.DEFAULT_DOMAIN)
self.domain1 = create_domain(
{'id': uuid.uuid4().hex, 'name': 'domain1'})
self.domain2 = create_domain(
{'id': uuid.uuid4().hex, 'name': 'domain2'})
self.domain3 = create_domain(
{'id': uuid.uuid4().hex, 'name': 'domain3'})
self.domain4 = create_domain(
{'id': uuid.uuid4().hex, 'name': 'domain4'})
def _set_domain_configs(self):
# We need to load the domain configs explicitly to ensure the
# test overrides are included.
self.identity_api.domain_configs._load_config(
self.identity_api.assignment_api,
[test.etcdir('keystone.conf.sample'),
test.testsdir('test_overrides.conf'),
test.testsdir('backend_multi_ldap_sql.conf'),
test.testsdir('keystone.Default.conf')],
'Default')
self.identity_api.domain_configs._load_config(
self.identity_api.assignment_api,
[test.etcdir('keystone.conf.sample'),
test.testsdir('test_overrides.conf'),
test.testsdir('backend_multi_ldap_sql.conf'),
test.testsdir('keystone.domain1.conf')],
'domain1')
self.identity_api.domain_configs._load_config(
self.identity_api.assignment_api,
[test.etcdir('keystone.conf.sample'),
test.testsdir('test_overrides.conf'),
test.testsdir('backend_multi_ldap_sql.conf'),
test.testsdir('keystone.domain2.conf')],
'domain2')
def reload_backends(self, domain_id):
# Just reload the driver for this domain - which will pickup
# any updated cfg
self.identity_api.domain_configs.reload_domain_driver(
self.identity_api.assignment_api, domain_id)
def get_config(self, domain_id):
# Get the config for this domain, will return CONF
# if no specific config defined for this domain
return self.identity_api.domain_configs.get_domain_conf(domain_id)
def test_list_domains(self):
self.skipTest(
'N/A: Not relevant for multi ldap testing')
def test_domain_segregation(self):
"""Test that separate configs have segregated the domain.
Test Plan:
- Create a user in each of the domains
- Make sure that you can only find a given user in its
relevant domain
- Make sure that for a backend that supports multiple domains
you can get the users via any of the domain scopes
"""
def create_user(domain_id):
user = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain_id,
'password': uuid.uuid4().hex,
'enabled': True}
self.identity_api.create_user(user['id'], user)
return user
userd = create_user(CONF.identity.default_domain_id)
user1 = create_user(self.domain1['id'])
user2 = create_user(self.domain2['id'])
user3 = create_user(self.domain3['id'])
user4 = create_user(self.domain4['id'])
# Now check that I can read user1 with the appropriate domain
# scope, but won't find it if the wrong scope is used
ref = self.identity_api.get_user(
userd['id'], domain_scope=CONF.identity.default_domain_id)
del userd['password']
self.assertDictEqual(ref, userd)
self.assertRaises(exception.UserNotFound,
self.identity_api.get_user,
userd['id'],
domain_scope=self.domain1['id'])
self.assertRaises(exception.UserNotFound,
self.identity_api.get_user,
userd['id'],
domain_scope=self.domain2['id'])
self.assertRaises(exception.UserNotFound,
self.identity_api.get_user,
userd['id'],
domain_scope=self.domain3['id'])
self.assertRaises(exception.UserNotFound,
self.identity_api.get_user,
userd['id'],
domain_scope=self.domain4['id'])
ref = self.identity_api.get_user(
user1['id'], domain_scope=self.domain1['id'])
del user1['password']
self.assertDictEqual(ref, user1)
ref = self.identity_api.get_user(
user2['id'], domain_scope=self.domain2['id'])
del user2['password']
self.assertDictEqual(ref, user2)
# Domains 3 and 4 share the same backend, so you should be
# able to see user3 and 4 from either
ref = self.identity_api.get_user(
user3['id'], domain_scope=self.domain3['id'])
del user3['password']
self.assertDictEqual(ref, user3)
ref = self.identity_api.get_user(
user4['id'], domain_scope=self.domain4['id'])
del user4['password']
self.assertDictEqual(ref, user4)
ref = self.identity_api.get_user(
user3['id'], domain_scope=self.domain4['id'])
self.assertDictEqual(ref, user3)
ref = self.identity_api.get_user(
user4['id'], domain_scope=self.domain3['id'])
self.assertDictEqual(ref, user4)
def test_scanning_of_config_dir(self):
"""Test the Manager class scans the config directory.
The setup for the main tests above load the domain configs directly
so that the test overrides can be included. This test just makes sure
that the standard config directory scanning does pick up the relevant
domain config files.
"""
# Confirm that config has drivers_enabled as True, which we will
# check has been set to False later in this test
self.assertTrue(config.CONF.identity.domain_specific_drivers_enabled)
self.load_backends()
# Execute any command to trigger the lazy loading of domain configs
self.identity_api.list_users(domain_scope=self.domain1['id'])
# ...and now check the domain configs have been set up
self.assertIn('default', self.identity_api.domain_configs)
self.assertIn(self.domain1['id'], self.identity_api.domain_configs)
self.assertIn(self.domain2['id'], self.identity_api.domain_configs)
self.assertNotIn(self.domain3['id'], self.identity_api.domain_configs)
self.assertNotIn(self.domain4['id'], self.identity_api.domain_configs)
# Finally check that a domain specific config contains items from both
# the primary config and the domain specific config
conf = self.identity_api.domain_configs.get_domain_conf(
self.domain1['id'])
# This should now be false, as is the default, since this is not
# set in the standard primary config file
self.assertFalse(conf.identity.domain_specific_drivers_enabled)
# ..and make sure a domain-specifc options is also set
self.assertEqual(conf.ldap.url, 'fake://memory1')

View File

@ -29,8 +29,6 @@ from keystone import token
CONF = config.CONF
config.register_str('servers', group='memcache', default='localhost:11211')
config.register_int('max_compare_and_set_retry', group='memcache', default=16)
LOG = logging.getLogger(__name__)

View File

@ -29,7 +29,7 @@ from keystone.openstack.common import timeutils
CONF = config.CONF
config.register_int('expiration', group='token', default=86400)
LOG = logging.getLogger(__name__)