Adding support for ldap connection pooling.
Using ldappool library to establish connection pooling. Connection pooling is disabled by default. Pooling specific configuration parameters are added in ldap section. Added pool test using existing FakeLdap as connector class. Added pool specific ldap live test. These tests are executed similar to existing ldap live test. Addressed async search_s and result3 API issues mentioned in review. Added separate connection pool for end user auth bind done by keystone identity ldap driver logic to avoid saturation of pool by these kind of binds and limiting pool effectiveness for other ldap operations. Rebased with lastest master and addressed doc comments. Change-Id: If516a0d308a7f3be88df5583a30739a935076173 Closes-Bug: #1320997 bp: ldap-connection-pooling DocImpact
This commit is contained in:
parent
686597b52a
commit
ea689ff78f
|
@ -1603,3 +1603,67 @@ section::
|
|||
user_allow_create = False
|
||||
user_allow_update = False
|
||||
user_allow_delete = False
|
||||
|
||||
Connection Pooling
|
||||
------------------
|
||||
|
||||
Various LDAP backends in keystone use a common LDAP module to interact with
|
||||
LDAP data. By default, a new connection is established for LDAP operations.
|
||||
This can become highly expensive when TLS support is enabled which is a likely
|
||||
configuraton in enterprise setup. Re-using of connectors from a connection
|
||||
pool drastically reduces overhead of initiating a new connection for every
|
||||
LDAP operation.
|
||||
|
||||
Keystone now provides connection pool support via configuration. This change
|
||||
will keep LDAP connectors alive and re-use for subsequent LDAP operations. A
|
||||
connection lifespan is going to be configurable with other pooling specific
|
||||
attributes. The change is made in LDAP handler layer logic which is primarily
|
||||
responsible for LDAP connection and shared common operations.
|
||||
|
||||
In LDAP identity driver, Keystone authenticates end user by LDAP bind with user
|
||||
DN and provided password. These kind of auth binds can fill up the pool pretty
|
||||
quickly so a separate pool is provided for those end user auth bind calls.
|
||||
If a deployment does not want to use pool for those binds, then it can disable
|
||||
pooling selectively by ``use_auth_pool`` as false. If a deployment wants to
|
||||
use pool for those auth binds, then ``use_auth_pool`` needs to be true. For
|
||||
auth pool, a different pool size (``auth_pool_size``) and connection lifetime
|
||||
(``auth_pool_connection_lifetime``) can be specified. With enabled auth pool,
|
||||
its connection lifetime should be kept short so that pool frequently re-binds
|
||||
the connection with provided creds and works reliably in end user password
|
||||
change case. When ``use_pool`` is false (disabled), then auth pool
|
||||
configuration is also not used.
|
||||
|
||||
Connection pool configuration is added in ``[ldap]`` configuration section::
|
||||
|
||||
[ldap]
|
||||
# Enable LDAP connection pooling. (boolean value)
|
||||
use_pool=false
|
||||
|
||||
# Connection pool size. (integer value)
|
||||
pool_size=10
|
||||
|
||||
# Maximum count of reconnect trials. (integer value)
|
||||
pool_retry_max=3
|
||||
|
||||
# Time span in seconds to wait between two reconnect trials.
|
||||
# (floating point value)
|
||||
pool_retry_delay=0.1
|
||||
|
||||
# Connector timeout in seconds. Value -1 indicates indefinite wait for
|
||||
# response. (integer value)
|
||||
pool_connection_timeout=-1
|
||||
|
||||
# Connection lifetime in seconds. (integer value)
|
||||
pool_connection_lifetime=600
|
||||
|
||||
# Enable LDAP connection pooling for end user authentication. If use_pool
|
||||
# is disabled, then this setting is meaningless and is not used at all.
|
||||
# (boolean value)
|
||||
use_auth_pool=false
|
||||
|
||||
# End user auth connection pool size. (integer value)
|
||||
auth_pool_size=100
|
||||
|
||||
# End user auth connection lifetime in seconds. (integer value)
|
||||
auth_pool_connection_lifetime=60
|
||||
|
||||
|
|
|
@ -354,9 +354,11 @@ and set environment variables ``KEYSTONE_IDENTITY_BACKEND=ldap`` and
|
|||
``KEYSTONE_CLEAR_LDAP=yes`` in your ``localrc`` file.
|
||||
|
||||
The unit tests can be run against a live server with
|
||||
``keystone/tests/test_ldap_livetest.py``. The default password is ``test`` but if you have
|
||||
installed devstack with a different LDAP password, modify the file
|
||||
``keystone/tests/backend_liveldap.conf`` to reflect your password.
|
||||
``keystone/tests/test_ldap_livetest.py`` and
|
||||
``keystone/tests/test_ldap_pool_livetest.py``. The default password is ``test``
|
||||
but if you have installed devstack with a different LDAP password, modify the
|
||||
file ``keystone/tests/config_files/backend_liveldap.conf`` and
|
||||
``keystone/tests/config_files/backend_pool_liveldap.conf`` to reflect your password.
|
||||
|
||||
.. NOTE::
|
||||
To run the live tests you need to set the environment variable ``ENABLE_LDAP_LIVE_TEST``
|
||||
|
|
|
@ -1149,6 +1149,38 @@
|
|||
# (string value)
|
||||
#tls_req_cert=demand
|
||||
|
||||
# Enable LDAP connection pooling. (boolean value)
|
||||
#use_pool=false
|
||||
|
||||
# Connection pool size. (integer value)
|
||||
#pool_size=10
|
||||
|
||||
# Maximum count of reconnect trials. (integer value)
|
||||
#pool_retry_max=3
|
||||
|
||||
# Time span in seconds to wait between two reconnect trials.
|
||||
# (floating point value)
|
||||
#pool_retry_delay=0.1
|
||||
|
||||
# Connector timeout in seconds. Value -1 indicates indefinite
|
||||
# wait for response. (integer value)
|
||||
#pool_connection_timeout=-1
|
||||
|
||||
# Connection lifetime in seconds. (integer value)
|
||||
#pool_connection_lifetime=600
|
||||
|
||||
# Enable LDAP connection pooling for end user authentication.
|
||||
# If use_pool is disabled, then this setting is meaningless
|
||||
# and is not used at all. (boolean value)
|
||||
#use_auth_pool=false
|
||||
|
||||
# End user auth connection pool size. (integer value)
|
||||
#auth_pool_size=100
|
||||
|
||||
# End user auth connection lifetime in seconds. (integer
|
||||
# value)
|
||||
#auth_pool_connection_lifetime=60
|
||||
|
||||
|
||||
[matchmaker_ring]
|
||||
|
||||
|
|
|
@ -706,6 +706,28 @@ FILE_OPTIONS = {
|
|||
cfg.StrOpt('tls_req_cert', default='demand',
|
||||
help='Valid options for tls_req_cert are demand, never, '
|
||||
'and allow.'),
|
||||
cfg.BoolOpt('use_pool', default=False,
|
||||
help='Enable LDAP connection pooling.'),
|
||||
cfg.IntOpt('pool_size', default=10,
|
||||
help='Connection pool size.'),
|
||||
cfg.IntOpt('pool_retry_max', default=3,
|
||||
help='Maximum count of reconnect trials.'),
|
||||
cfg.FloatOpt('pool_retry_delay', default=0.1,
|
||||
help='Time span in seconds to wait between two '
|
||||
'reconnect trials.'),
|
||||
cfg.IntOpt('pool_connection_timeout', default=-1,
|
||||
help='Connector timeout in seconds. Value -1 indicates '
|
||||
'indefinite wait for response.'),
|
||||
cfg.IntOpt('pool_connection_lifetime', default=600,
|
||||
help='Connection lifetime in seconds.'),
|
||||
cfg.BoolOpt('use_auth_pool', default=False,
|
||||
help='Enable LDAP connection pooling for end user '
|
||||
'authentication. If use_pool is disabled, then this '
|
||||
'setting is meaningless and is not used at all.'),
|
||||
cfg.IntOpt('auth_pool_size', default=100,
|
||||
help='End user auth connection pool size.'),
|
||||
cfg.IntOpt('auth_pool_connection_lifetime', default=60,
|
||||
help='End user auth connection lifetime in seconds.'),
|
||||
],
|
||||
'auth': [
|
||||
cfg.ListOpt('methods', default=_DEFAULT_AUTH_METHODS,
|
||||
|
|
|
@ -17,9 +17,13 @@ import os.path
|
|||
import re
|
||||
|
||||
import codecs
|
||||
import functools
|
||||
import ldap
|
||||
import ldap.filter
|
||||
import ldappool
|
||||
import six
|
||||
import sys
|
||||
import weakref
|
||||
|
||||
from keystone import exception
|
||||
from keystone.i18n import _
|
||||
|
@ -27,7 +31,6 @@ from keystone.openstack.common import log
|
|||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
LDAP_VALUES = {'TRUE': True, 'FALSE': False}
|
||||
CONTROL_TREEDELETE = '1.2.840.113556.1.4.805'
|
||||
LDAP_SCOPES = {'one': ldap.SCOPE_ONELEVEL,
|
||||
|
@ -404,7 +407,10 @@ class LDAPHandler(object):
|
|||
@abc.abstractmethod
|
||||
def connect(self, url, page_size=0, alias_dereferencing=None,
|
||||
use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
|
||||
tls_req_cert='demand', chase_referrals=None, debug_level=None):
|
||||
tls_req_cert='demand', chase_referrals=None, debug_level=None,
|
||||
use_pool=None, pool_size=None, pool_retry_max=None,
|
||||
pool_retry_delay=None, pool_conn_timeout=None,
|
||||
pool_conn_lifetime=None):
|
||||
raise exception.NotImplemented() # pragma: no cover
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -472,54 +478,17 @@ class PythonLDAPHandler(LDAPHandler):
|
|||
|
||||
def connect(self, url, page_size=0, alias_dereferencing=None,
|
||||
use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
|
||||
tls_req_cert='demand', chase_referrals=None, debug_level=None):
|
||||
LOG.debug("LDAP init: url=%s", url)
|
||||
LOG.debug('LDAP init: use_tls=%s tls_cacertfile=%s tls_cacertdir=%s '
|
||||
'tls_req_cert=%s tls_avail=%s',
|
||||
use_tls, tls_cacertfile, tls_cacertdir,
|
||||
tls_req_cert, ldap.TLS_AVAIL)
|
||||
tls_req_cert='demand', chase_referrals=None, debug_level=None,
|
||||
use_pool=None, pool_size=None, pool_retry_max=None,
|
||||
pool_retry_delay=None, pool_conn_timeout=None,
|
||||
pool_conn_lifetime=None):
|
||||
|
||||
if debug_level is not None:
|
||||
ldap.set_option(ldap.OPT_DEBUG_LEVEL, debug_level)
|
||||
|
||||
using_ldaps = url.lower().startswith("ldaps")
|
||||
|
||||
if use_tls and using_ldaps:
|
||||
raise AssertionError(_('Invalid TLS / LDAPS combination'))
|
||||
|
||||
if use_tls:
|
||||
if not ldap.TLS_AVAIL:
|
||||
raise ValueError(_('Invalid LDAP TLS_AVAIL option: %s. TLS '
|
||||
'not available') % ldap.TLS_AVAIL)
|
||||
if tls_cacertfile:
|
||||
# NOTE(topol)
|
||||
# python ldap TLS does not verify CACERTFILE or CACERTDIR
|
||||
# so we add some extra simple sanity check verification
|
||||
# Also, setting these values globally (i.e. on the ldap object)
|
||||
# works but these values are ignored when setting them on the
|
||||
# connection
|
||||
if not os.path.isfile(tls_cacertfile):
|
||||
raise IOError(_("tls_cacertfile %s not found "
|
||||
"or is not a file") %
|
||||
tls_cacertfile)
|
||||
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile)
|
||||
elif tls_cacertdir:
|
||||
# NOTE(topol)
|
||||
# python ldap TLS does not verify CACERTFILE or CACERTDIR
|
||||
# so we add some extra simple sanity check verification
|
||||
# Also, setting these values globally (i.e. on the ldap object)
|
||||
# works but these values are ignored when setting them on the
|
||||
# connection
|
||||
if not os.path.isdir(tls_cacertdir):
|
||||
raise IOError(_("tls_cacertdir %s not found "
|
||||
"or is not a directory") %
|
||||
tls_cacertdir)
|
||||
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir)
|
||||
if tls_req_cert in LDAP_TLS_CERTS.values():
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert)
|
||||
else:
|
||||
LOG.debug("LDAP TLS: invalid TLS_REQUIRE_CERT Option=%s",
|
||||
tls_req_cert)
|
||||
_common_ldap_initialization(url=url,
|
||||
use_tls=use_tls,
|
||||
tls_cacertfile=tls_cacertfile,
|
||||
tls_cacertdir=tls_cacertdir,
|
||||
tls_req_cert=tls_req_cert,
|
||||
debug_level=debug_level)
|
||||
|
||||
self.conn = ldap.initialize(url)
|
||||
self.conn.protocol_version = ldap.VERSION3
|
||||
|
@ -581,6 +550,269 @@ class PythonLDAPHandler(LDAPHandler):
|
|||
return self.conn.delete_ext_s(dn, serverctrls, clientctrls)
|
||||
|
||||
|
||||
def _common_ldap_initialization(url, use_tls=False, tls_cacertfile=None,
|
||||
tls_cacertdir=None, tls_req_cert=None,
|
||||
debug_level=None):
|
||||
'''Method for common ldap initialization between PythonLDAPHandler and
|
||||
PooledLDAPHandler.
|
||||
'''
|
||||
|
||||
LOG.debug("LDAP init: url=%s", url)
|
||||
LOG.debug('LDAP init: use_tls=%s tls_cacertfile=%s tls_cacertdir=%s '
|
||||
'tls_req_cert=%s tls_avail=%s',
|
||||
use_tls, tls_cacertfile, tls_cacertdir,
|
||||
tls_req_cert, ldap.TLS_AVAIL)
|
||||
|
||||
if debug_level is not None:
|
||||
ldap.set_option(ldap.OPT_DEBUG_LEVEL, debug_level)
|
||||
|
||||
using_ldaps = url.lower().startswith("ldaps")
|
||||
|
||||
if use_tls and using_ldaps:
|
||||
raise AssertionError(_('Invalid TLS / LDAPS combination'))
|
||||
|
||||
if use_tls:
|
||||
if not ldap.TLS_AVAIL:
|
||||
raise ValueError(_('Invalid LDAP TLS_AVAIL option: %s. TLS '
|
||||
'not available') % ldap.TLS_AVAIL)
|
||||
if tls_cacertfile:
|
||||
# NOTE(topol)
|
||||
# python ldap TLS does not verify CACERTFILE or CACERTDIR
|
||||
# so we add some extra simple sanity check verification
|
||||
# Also, setting these values globally (i.e. on the ldap object)
|
||||
# works but these values are ignored when setting them on the
|
||||
# connection
|
||||
if not os.path.isfile(tls_cacertfile):
|
||||
raise IOError(_("tls_cacertfile %s not found "
|
||||
"or is not a file") %
|
||||
tls_cacertfile)
|
||||
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile)
|
||||
elif tls_cacertdir:
|
||||
# NOTE(topol)
|
||||
# python ldap TLS does not verify CACERTFILE or CACERTDIR
|
||||
# so we add some extra simple sanity check verification
|
||||
# Also, setting these values globally (i.e. on the ldap object)
|
||||
# works but these values are ignored when setting them on the
|
||||
# connection
|
||||
if not os.path.isdir(tls_cacertdir):
|
||||
raise IOError(_("tls_cacertdir %s not found "
|
||||
"or is not a directory") %
|
||||
tls_cacertdir)
|
||||
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir)
|
||||
if tls_req_cert in LDAP_TLS_CERTS.values():
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert)
|
||||
else:
|
||||
LOG.debug("LDAP TLS: invalid TLS_REQUIRE_CERT Option=%s",
|
||||
tls_req_cert)
|
||||
|
||||
|
||||
class MsgId(list):
|
||||
'''Wrapper class to hold connection and msgid.'''
|
||||
pass
|
||||
|
||||
|
||||
def use_conn_pool(func):
|
||||
'''Use this only for connection pool specific ldap API.
|
||||
|
||||
This adds connection object to decorated API as next argument after self.
|
||||
'''
|
||||
def wrapper(self, *args, **kwargs):
|
||||
# assert isinstance(self, PooledLDAPHandler)
|
||||
with self._get_pool_connection() as conn:
|
||||
self._apply_options(conn)
|
||||
return func(self, conn, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class PooledLDAPHandler(LDAPHandler):
|
||||
'''Implementation of the LDAPHandler interface which uses pooled
|
||||
connection manager.
|
||||
|
||||
Pool specific configuration is defined in [ldap] section.
|
||||
All other LDAP configuration is still used from [ldap] section
|
||||
|
||||
Keystone LDAP authentication logic authenticates an end user using its DN
|
||||
and password via LDAP bind to establish supplied password is correct.
|
||||
This can fill up the pool quickly (as pool re-uses existing connection
|
||||
based on its bind data) and would not leave space in pool for connection
|
||||
re-use for other LDAP operations.
|
||||
Now a separate pool can be established for those requests when related flag
|
||||
'use_auth_pool' is enabled. That pool can have its own size and
|
||||
connection lifetime. Other pool attributes are shared between those pools.
|
||||
If 'use_pool' is disabled, then 'use_auth_pool' does not matter.
|
||||
If 'use_auth_pool' is not enabled, then connection pooling is not used for
|
||||
those LDAP operations.
|
||||
|
||||
Note, the python-ldap API requires all string values to be UTF-8
|
||||
encoded. The KeystoneLDAPHandler enforces this prior to invoking
|
||||
the methods in this class.
|
||||
'''
|
||||
|
||||
# Added here to allow override for testing
|
||||
Connector = ldappool.StateConnector
|
||||
auth_pool_prefix = 'auth_pool_'
|
||||
|
||||
connection_pools = {} # static connector pool dict
|
||||
|
||||
def __init__(self, conn=None, use_auth_pool=False):
|
||||
super(PooledLDAPHandler, self).__init__(conn=conn)
|
||||
self.who = ''
|
||||
self.cred = ''
|
||||
self.conn_options = {} # connection specific options
|
||||
self.page_size = None
|
||||
self.use_auth_pool = use_auth_pool
|
||||
self.conn_pool = None
|
||||
|
||||
def connect(self, url, page_size=0, alias_dereferencing=None,
|
||||
use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
|
||||
tls_req_cert='demand', chase_referrals=None, debug_level=None,
|
||||
use_pool=None, pool_size=None, pool_retry_max=None,
|
||||
pool_retry_delay=None, pool_conn_timeout=None,
|
||||
pool_conn_lifetime=None):
|
||||
|
||||
_common_ldap_initialization(url=url,
|
||||
use_tls=use_tls,
|
||||
tls_cacertfile=tls_cacertfile,
|
||||
tls_cacertdir=tls_cacertdir,
|
||||
tls_req_cert=tls_req_cert,
|
||||
debug_level=debug_level)
|
||||
|
||||
self.page_size = page_size
|
||||
|
||||
# Following two options are not added in common initialization as they
|
||||
# need to follow a sequence in PythonLDAPHandler code.
|
||||
if alias_dereferencing is not None:
|
||||
self.set_option(ldap.OPT_DEREF, alias_dereferencing)
|
||||
if chase_referrals is not None:
|
||||
self.set_option(ldap.OPT_REFERRALS, int(chase_referrals))
|
||||
|
||||
if self.use_auth_pool: # separate pool when use_auth_pool enabled
|
||||
pool_url = self.auth_pool_prefix + url
|
||||
else:
|
||||
pool_url = url
|
||||
try:
|
||||
self.conn_pool = self.connection_pools[pool_url]
|
||||
except KeyError:
|
||||
self.conn_pool = ldappool.ConnectionManager(
|
||||
url,
|
||||
size=pool_size,
|
||||
retry_max=pool_retry_max,
|
||||
retry_delay=pool_retry_delay,
|
||||
timeout=pool_conn_timeout,
|
||||
connector_cls=self.Connector,
|
||||
use_tls=use_tls,
|
||||
max_lifetime=pool_conn_lifetime)
|
||||
self.connection_pools[pool_url] = self.conn_pool
|
||||
|
||||
def set_option(self, option, invalue):
|
||||
self.conn_options[option] = invalue
|
||||
|
||||
def get_option(self, option):
|
||||
value = self.conn_options.get(option)
|
||||
# if option was not specified explictly, then use connection default
|
||||
# value for that option if there.
|
||||
if value is None:
|
||||
with self._get_pool_connection() as conn:
|
||||
value = conn.get_option(option)
|
||||
return value
|
||||
|
||||
def _apply_options(self, conn):
|
||||
# if connection has a lifetime, then it already has options specified
|
||||
if conn.get_lifetime() > 30:
|
||||
return
|
||||
for option, invalue in six.iteritems(self.conn_options):
|
||||
conn.set_option(option, invalue)
|
||||
|
||||
def _get_pool_connection(self):
|
||||
return self.conn_pool.connection(self.who, self.cred)
|
||||
|
||||
def simple_bind_s(self, who='', cred='',
|
||||
serverctrls=None, clientctrls=None):
|
||||
'''Not using use_conn_pool decorator here as this API takes cred as
|
||||
input.
|
||||
'''
|
||||
self.who = who
|
||||
self.cred = cred
|
||||
with self._get_pool_connection() as conn:
|
||||
self._apply_options(conn)
|
||||
|
||||
def unbind_s(self):
|
||||
# After connection generator is done `with` statement execution block
|
||||
# connection is always released via finally block in ldappool.
|
||||
# So this unbind is a no op.
|
||||
pass
|
||||
|
||||
@use_conn_pool
|
||||
def add_s(self, conn, dn, modlist):
|
||||
return conn.add_s(dn, modlist)
|
||||
|
||||
@use_conn_pool
|
||||
def search_s(self, conn, base, scope,
|
||||
filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
|
||||
return conn.search_s(base, scope, filterstr, attrlist,
|
||||
attrsonly)
|
||||
|
||||
def search_ext(self, base, scope,
|
||||
filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
|
||||
serverctrls=None, clientctrls=None,
|
||||
timeout=-1, sizelimit=0):
|
||||
'''This API is asynchoronus API which returns MsgId instance to be used
|
||||
in result3 call.
|
||||
|
||||
To work with result3 API in predicatable manner, same LDAP connection
|
||||
is needed which provided msgid. So wrapping used connection and msgid
|
||||
in MsgId class. The connection associated with search_ext is released
|
||||
once last hard reference to MsgId object is freed. This will happen
|
||||
when the method is done with returned MsgId usage.
|
||||
'''
|
||||
|
||||
conn_ctxt = self._get_pool_connection()
|
||||
conn = conn_ctxt.__enter__()
|
||||
try:
|
||||
msgid = conn.search_ext(base, scope,
|
||||
filterstr, attrlist, attrsonly,
|
||||
serverctrls, clientctrls,
|
||||
timeout, sizelimit)
|
||||
except Exception:
|
||||
conn_ctxt.__exit__(*sys.exc_info())
|
||||
raise
|
||||
res = MsgId((conn, msgid))
|
||||
weakref.ref(res, functools.partial(conn_ctxt.__exit__,
|
||||
None, None, None))
|
||||
return res
|
||||
|
||||
def result3(self, msgid, all=1, timeout=None,
|
||||
resp_ctrl_classes=None):
|
||||
'''This method is used to wait for and return the result of an
|
||||
operation previously initiated by one of the LDAP asynchronous
|
||||
operation routines (eg search_ext()) It returned an invocation
|
||||
identifier (a message id) upon successful initiation of their
|
||||
operation.
|
||||
|
||||
Input msgid is expected to be instance of class MsgId which has LDAP
|
||||
session/connection used to execute search_ext and message idenfier.
|
||||
|
||||
The connection associated with search_ext is released once last hard
|
||||
reference to MsgId object is freed. This will happen when function
|
||||
which requested msgId and used it in result3 exits.
|
||||
'''
|
||||
|
||||
conn, msg_id = msgid
|
||||
return conn.result3(msg_id, all, timeout)
|
||||
|
||||
@use_conn_pool
|
||||
def modify_s(self, conn, dn, modlist):
|
||||
return conn.modify_s(dn, modlist)
|
||||
|
||||
@use_conn_pool
|
||||
def delete_s(self, conn, dn):
|
||||
return conn.delete_s(dn)
|
||||
|
||||
@use_conn_pool
|
||||
def delete_ext_s(self, conn, dn, serverctrls=None, clientctrls=None):
|
||||
return conn.delete_ext_s(dn, serverctrls, clientctrls)
|
||||
|
||||
|
||||
class KeystoneLDAPHandler(LDAPHandler):
|
||||
'''Convert data types and perform logging.
|
||||
|
||||
|
@ -617,11 +849,21 @@ class KeystoneLDAPHandler(LDAPHandler):
|
|||
|
||||
def connect(self, url, page_size=0, alias_dereferencing=None,
|
||||
use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
|
||||
tls_req_cert='demand', chase_referrals=None, debug_level=None):
|
||||
tls_req_cert='demand', chase_referrals=None, debug_level=None,
|
||||
use_pool=None, pool_size=None,
|
||||
pool_retry_max=None, pool_retry_delay=None,
|
||||
pool_conn_timeout=None, pool_conn_lifetime=None):
|
||||
self.page_size = page_size
|
||||
return self.conn.connect(url, page_size, alias_dereferencing,
|
||||
use_tls, tls_cacertfile, tls_cacertdir,
|
||||
tls_req_cert, chase_referrals,
|
||||
debug_level=debug_level)
|
||||
debug_level=debug_level,
|
||||
use_pool=use_pool,
|
||||
pool_size=pool_size,
|
||||
pool_retry_max=pool_retry_max,
|
||||
pool_retry_delay=pool_retry_delay,
|
||||
pool_conn_timeout=pool_conn_timeout,
|
||||
pool_conn_lifetime=pool_conn_lifetime)
|
||||
|
||||
def set_option(self, option, invalue):
|
||||
return self.conn.set_option(option, invalue)
|
||||
|
@ -635,7 +877,8 @@ class KeystoneLDAPHandler(LDAPHandler):
|
|||
who_utf8 = utf8_encode(who)
|
||||
cred_utf8 = utf8_encode(cred)
|
||||
return self.conn.simple_bind_s(who_utf8, cred_utf8,
|
||||
serverctrls, clientctrls)
|
||||
serverctrls=serverctrls,
|
||||
clientctrls=clientctrls)
|
||||
|
||||
def unbind_s(self):
|
||||
LOG.debug("LDAP unbind")
|
||||
|
@ -798,12 +1041,15 @@ def register_handler(prefix, handler):
|
|||
_HANDLERS[prefix] = handler
|
||||
|
||||
|
||||
def _get_connection(conn_url):
|
||||
def _get_connection(conn_url, use_pool=False, use_auth_pool=False):
|
||||
for prefix, handler in six.iteritems(_HANDLERS):
|
||||
if conn_url.startswith(prefix):
|
||||
return handler()
|
||||
|
||||
return PythonLDAPHandler()
|
||||
if use_pool:
|
||||
return PooledLDAPHandler(use_auth_pool=use_auth_pool)
|
||||
else:
|
||||
return PythonLDAPHandler()
|
||||
|
||||
|
||||
def filter_entity(entity_ref):
|
||||
|
@ -854,6 +1100,19 @@ class BaseLdap(object):
|
|||
self.chase_referrals = conf.ldap.chase_referrals
|
||||
self.debug_level = conf.ldap.debug_level
|
||||
|
||||
# LDAP Pool specific attribute
|
||||
self.use_pool = conf.ldap.use_pool
|
||||
self.pool_size = conf.ldap.pool_size
|
||||
self.pool_retry_max = conf.ldap.pool_retry_max
|
||||
self.pool_retry_delay = conf.ldap.pool_retry_delay
|
||||
self.pool_conn_timeout = conf.ldap.pool_connection_timeout
|
||||
self.pool_conn_lifetime = conf.ldap.pool_connection_lifetime
|
||||
|
||||
# End user authentication pool specific config attributes
|
||||
self.use_auth_pool = self.use_pool and conf.ldap.use_auth_pool
|
||||
self.auth_pool_size = conf.ldap.auth_pool_size
|
||||
self.auth_pool_conn_lifetime = conf.ldap.auth_pool_connection_lifetime
|
||||
|
||||
if self.options_name is not None:
|
||||
self.suffix = conf.ldap.suffix
|
||||
if self.suffix is None:
|
||||
|
@ -938,8 +1197,20 @@ class BaseLdap(object):
|
|||
return (self.use_dumb_member
|
||||
and is_dn_equal(member_dn, self.dumb_member))
|
||||
|
||||
def get_connection(self, user=None, password=None):
|
||||
conn = _get_connection(self.LDAP_URL)
|
||||
def get_connection(self, user=None, password=None, end_user_auth=False):
|
||||
use_pool = self.use_pool
|
||||
pool_size = self.pool_size
|
||||
pool_conn_lifetime = self.pool_conn_lifetime
|
||||
|
||||
if end_user_auth:
|
||||
if not self.use_auth_pool:
|
||||
use_pool = False
|
||||
else:
|
||||
pool_size = self.auth_pool_size
|
||||
pool_conn_lifetime = self.auth_pool_conn_lifetime
|
||||
|
||||
conn = _get_connection(self.LDAP_URL, use_pool,
|
||||
use_auth_pool=end_user_auth)
|
||||
|
||||
conn = KeystoneLDAPHandler(conn=conn)
|
||||
|
||||
|
@ -951,7 +1222,14 @@ class BaseLdap(object):
|
|||
tls_cacertdir=self.tls_cacertdir,
|
||||
tls_req_cert=self.tls_req_cert,
|
||||
chase_referrals=self.chase_referrals,
|
||||
debug_level=self.debug_level)
|
||||
debug_level=self.debug_level,
|
||||
use_pool=use_pool,
|
||||
pool_size=pool_size,
|
||||
pool_retry_max=self.pool_retry_max,
|
||||
pool_retry_delay=self.pool_retry_delay,
|
||||
pool_conn_timeout=self.pool_conn_timeout,
|
||||
pool_conn_lifetime=pool_conn_lifetime
|
||||
)
|
||||
|
||||
if user is None:
|
||||
user = self.LDAP_USER
|
||||
|
|
|
@ -64,7 +64,7 @@ class Identity(identity.Driver):
|
|||
conn = None
|
||||
try:
|
||||
conn = self.user.get_connection(user_ref['dn'],
|
||||
password)
|
||||
password, end_user_auth=True)
|
||||
if not conn:
|
||||
raise AssertionError(_('Invalid user / password'))
|
||||
except Exception:
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
[ldap]
|
||||
url = fakepool://memory
|
||||
user = cn=Admin
|
||||
password = password
|
||||
backend_entities = ['Tenant', 'User', 'UserRoleAssociation', 'Role', 'Group', 'Domain']
|
||||
suffix = cn=example,cn=com
|
||||
|
||||
# Connection pooling specific attributes
|
||||
|
||||
# Enable LDAP connection pooling. (boolean value)
|
||||
use_pool=true
|
||||
|
||||
# Connection pool size. (integer value)
|
||||
pool_size=5
|
||||
|
||||
# Maximum count of reconnect trials. (integer value)
|
||||
pool_retry_max=2
|
||||
|
||||
# Time span in seconds to wait between two reconnect trials.
|
||||
# (floating point value)
|
||||
pool_retry_delay=0.2
|
||||
|
||||
# Connector timeout in seconds. Value -1 indicates indefinite
|
||||
# wait for response. (integer value)
|
||||
pool_connection_timeout=-1
|
||||
|
||||
# Connection lifetime in seconds.
|
||||
# (integer value)
|
||||
pool_connection_lifetime=600
|
||||
|
||||
# Enable LDAP connection pooling for end user authentication.
|
||||
# If use_pool is disabled, then this setting is meaningless
|
||||
# and is not used at all. (boolean value)
|
||||
use_auth_pool=true
|
||||
|
||||
# End user auth connection pool size. (integer value)
|
||||
auth_pool_size=50
|
||||
|
||||
# End user auth connection lifetime in seconds. (integer
|
||||
# value)
|
||||
auth_pool_connection_lifetime=60
|
|
@ -0,0 +1,35 @@
|
|||
[ldap]
|
||||
url = ldap://localhost
|
||||
user = cn=Manager,dc=openstack,dc=org
|
||||
password = test
|
||||
suffix = dc=openstack,dc=org
|
||||
group_tree_dn = ou=UserGroups,dc=openstack,dc=org
|
||||
role_tree_dn = ou=Roles,dc=openstack,dc=org
|
||||
project_tree_dn = ou=Projects,dc=openstack,dc=org
|
||||
user_tree_dn = ou=Users,dc=openstack,dc=org
|
||||
project_enabled_emulation = True
|
||||
user_enabled_emulation = True
|
||||
user_mail_attribute = mail
|
||||
use_dumb_member = True
|
||||
|
||||
# Connection pooling specific attributes
|
||||
|
||||
# Enable LDAP connection pooling. (boolean value)
|
||||
use_pool=true
|
||||
# Connection pool size. (integer value)
|
||||
pool_size=5
|
||||
# Connection lifetime in seconds.
|
||||
# (integer value)
|
||||
pool_connection_lifetime=60
|
||||
|
||||
# Enable LDAP connection pooling for end user authentication.
|
||||
# If use_pool is disabled, then this setting is meaningless
|
||||
# and is not used at all. (boolean value)
|
||||
use_auth_pool=true
|
||||
|
||||
# End user auth connection pool size. (integer value)
|
||||
auth_pool_size=50
|
||||
|
||||
# End user auth connection lifetime in seconds. (integer
|
||||
# value)
|
||||
auth_pool_connection_lifetime=300
|
|
@ -30,6 +30,7 @@ import six
|
|||
from six import moves
|
||||
|
||||
from keystone.common.ldap import core
|
||||
from keystone import config
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import log
|
||||
|
||||
|
@ -45,6 +46,7 @@ SCOPE_NAMES = {
|
|||
CONTROL_TREEDELETE = '1.2.840.113556.1.4.805'
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
def _internal_attr(attr_name, value_or_values):
|
||||
|
@ -200,7 +202,10 @@ class FakeLdap(core.LDAPHandler):
|
|||
|
||||
def connect(self, url, page_size=0, alias_dereferencing=None,
|
||||
use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
|
||||
tls_req_cert='demand', chase_referrals=None, debug_level=None):
|
||||
tls_req_cert='demand', chase_referrals=None, debug_level=None,
|
||||
use_pool=None, pool_size=None, pool_retry_max=None,
|
||||
pool_retry_delay=None, pool_conn_timeout=None,
|
||||
pool_conn_lifetime=None):
|
||||
if url.startswith('fake://memory'):
|
||||
if url not in FakeShelves:
|
||||
FakeShelves[url] = FakeShelve()
|
||||
|
@ -228,6 +233,13 @@ class FakeLdap(core.LDAPHandler):
|
|||
self.set_option(ldap.OPT_DEREF, alias_dereferencing)
|
||||
self.page_size = page_size
|
||||
|
||||
self.use_pool = use_pool
|
||||
self.pool_size = pool_size
|
||||
self.pool_retry_max = pool_retry_max
|
||||
self.pool_retry_delay = pool_retry_delay
|
||||
self.pool_conn_timeout = pool_conn_timeout
|
||||
self.pool_conn_lifetime = pool_conn_lifetime
|
||||
|
||||
def dn(self, dn):
|
||||
return core.utf8_decode(dn)
|
||||
|
||||
|
@ -239,7 +251,8 @@ class FakeLdap(core.LDAPHandler):
|
|||
"""This method is ignored, but provided for compatibility."""
|
||||
if server_fail:
|
||||
raise ldap.SERVER_DOWN
|
||||
if who == 'cn=Admin' and cred == 'password':
|
||||
whos = ['cn=Admin', CONF.ldap.user]
|
||||
if who in whos and cred in ['password', CONF.ldap.password]:
|
||||
return
|
||||
|
||||
try:
|
||||
|
@ -465,3 +478,43 @@ class FakeLdap(core.LDAPHandler):
|
|||
def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None,
|
||||
resp_ctrl_classes=None):
|
||||
raise exception.NotImplemented()
|
||||
|
||||
|
||||
class FakeLdapPool(FakeLdap):
|
||||
'''Emulate the python-ldap API with pooled connections using existing
|
||||
FakeLdap logic.
|
||||
|
||||
This class is used as connector class in PooledLDAPHandler.
|
||||
'''
|
||||
|
||||
def __init__(self, uri, retry_max=None, retry_delay=None, conn=None):
|
||||
super(FakeLdapPool, self).__init__(conn=conn)
|
||||
self.url = uri
|
||||
self.connected = None
|
||||
self.conn = self
|
||||
self._connection_time = 5 # any number greater than 0
|
||||
|
||||
def get_lifetime(self):
|
||||
return self._connection_time
|
||||
|
||||
def simple_bind_s(self, who=None, cred=None,
|
||||
serverctrls=None, clientctrls=None):
|
||||
if self.url.startswith('fakepool://memory'):
|
||||
if self.url not in FakeShelves:
|
||||
FakeShelves[self.url] = FakeShelve()
|
||||
self.db = FakeShelves[self.url]
|
||||
else:
|
||||
self.db = shelve.open(self.url[11:])
|
||||
|
||||
if not who:
|
||||
who = 'cn=Admin'
|
||||
if not cred:
|
||||
cred = 'password'
|
||||
|
||||
super(FakeLdapPool, self).simple_bind_s(who=who, cred=cred,
|
||||
serverctrls=serverctrls,
|
||||
clientctrls=clientctrls)
|
||||
|
||||
def unbind_ext_s(self):
|
||||
'''Added to extend FakeLdap as connector class.'''
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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 ldappool
|
||||
import mock
|
||||
|
||||
from keystone.common.ldap import core as ldap_core
|
||||
from keystone import config
|
||||
from keystone.identity.backends import ldap
|
||||
from keystone import tests
|
||||
from keystone.tests import fakeldap
|
||||
from keystone.tests import test_backend_ldap
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class LdapPoolCommonTestMixin(object):
|
||||
"""LDAP pool specific common tests used here and in live tests."""
|
||||
|
||||
def cleanup_pools(self):
|
||||
ldap_core.PooledLDAPHandler.connection_pools.clear()
|
||||
|
||||
def test_handler_with_use_pool_enabled(self):
|
||||
# by default use_pool and use_auth_pool is enabled in test pool config
|
||||
user_ref = self.identity_api.get_user(self.user_foo['id'])
|
||||
self.user_foo.pop('password')
|
||||
self.assertDictEqual(user_ref, self.user_foo)
|
||||
|
||||
handler = ldap_core._get_connection(CONF.ldap.url, use_pool=True)
|
||||
self.assertIsInstance(handler, ldap_core.PooledLDAPHandler)
|
||||
|
||||
@mock.patch.object(ldap_core.KeystoneLDAPHandler, 'connect')
|
||||
@mock.patch.object(ldap_core.KeystoneLDAPHandler, 'simple_bind_s')
|
||||
def test_handler_with_use_pool_not_enabled(self, bind_method,
|
||||
connect_method):
|
||||
self.config_fixture.config(group='ldap', use_pool=False)
|
||||
self.config_fixture.config(group='ldap', use_auth_pool=True)
|
||||
self.cleanup_pools()
|
||||
|
||||
user_api = ldap.UserApi(CONF)
|
||||
handler = user_api.get_connection(user=None, password=None,
|
||||
end_user_auth=True)
|
||||
# use_auth_pool flag does not matter when use_pool is False
|
||||
# still handler is non pool version
|
||||
self.assertIsInstance(handler.conn, ldap_core.PythonLDAPHandler)
|
||||
|
||||
@mock.patch.object(ldap_core.KeystoneLDAPHandler, 'connect')
|
||||
@mock.patch.object(ldap_core.KeystoneLDAPHandler, 'simple_bind_s')
|
||||
def test_handler_with_end_user_auth_use_pool_not_enabled(self, bind_method,
|
||||
connect_method):
|
||||
# by default use_pool is enabled in test pool config
|
||||
# now disabling use_auth_pool flag to test handler instance
|
||||
self.config_fixture.config(group='ldap', use_auth_pool=False)
|
||||
self.cleanup_pools()
|
||||
|
||||
user_api = ldap.UserApi(CONF)
|
||||
handler = user_api.get_connection(user=None, password=None,
|
||||
end_user_auth=True)
|
||||
self.assertIsInstance(handler.conn, ldap_core.PythonLDAPHandler)
|
||||
|
||||
# For end_user_auth case, flag should not be false otherwise
|
||||
# it will use, admin connections ldap pool
|
||||
handler = user_api.get_connection(user=None, password=None,
|
||||
end_user_auth=False)
|
||||
self.assertIsInstance(handler.conn, ldap_core.PooledLDAPHandler)
|
||||
|
||||
def test_pool_size_set(self):
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
self.assertEqual(CONF.ldap.pool_size, ldappool_cm.size)
|
||||
|
||||
def test_pool_retry_max_set(self):
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
self.assertEqual(CONF.ldap.pool_retry_max, ldappool_cm.retry_max)
|
||||
|
||||
def test_pool_retry_delay_set(self):
|
||||
# just make one identity call to initiate ldap connection if not there
|
||||
self.identity_api.get_user(self.user_foo['id'])
|
||||
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
self.assertEqual(CONF.ldap.pool_retry_delay, ldappool_cm.retry_delay)
|
||||
|
||||
def test_pool_use_tls_set(self):
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
self.assertEqual(CONF.ldap.use_tls, ldappool_cm.use_tls)
|
||||
|
||||
def test_pool_timeout_set(self):
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
self.assertEqual(CONF.ldap.pool_connection_timeout,
|
||||
ldappool_cm.timeout)
|
||||
|
||||
def test_pool_use_pool_set(self):
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
self.assertEqual(CONF.ldap.use_pool, ldappool_cm.use_pool)
|
||||
|
||||
def test_pool_connection_lifetime_set(self):
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
self.assertEqual(CONF.ldap.pool_connection_lifetime,
|
||||
ldappool_cm.max_lifetime)
|
||||
|
||||
def test_max_connection_error_raised(self):
|
||||
|
||||
who = CONF.ldap.user
|
||||
cred = CONF.ldap.password
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
ldappool_cm.size = 2
|
||||
|
||||
# 3rd connection attempt should raise Max connection error
|
||||
with ldappool_cm.connection(who, cred) as _: # conn1
|
||||
with ldappool_cm.connection(who, cred) as _: # conn2
|
||||
try:
|
||||
with ldappool_cm.connection(who, cred) as _: # conn3
|
||||
_.unbind_s()
|
||||
self.fail()
|
||||
except Exception as ex:
|
||||
self.assertIsInstance(ex,
|
||||
ldappool.MaxConnectionReachedError)
|
||||
ldappool_cm.size = CONF.ldap.pool_size
|
||||
|
||||
def test_pool_size_expands_correctly(self):
|
||||
|
||||
who = CONF.ldap.user
|
||||
cred = CONF.ldap.password
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
ldappool_cm.size = 3
|
||||
|
||||
def _get_conn():
|
||||
return ldappool_cm.connection(who, cred)
|
||||
|
||||
# Open 3 connections first
|
||||
with _get_conn() as _: # conn1
|
||||
self.assertEqual(len(ldappool_cm), 1)
|
||||
with _get_conn() as _: # conn2
|
||||
self.assertEqual(len(ldappool_cm), 2)
|
||||
with _get_conn() as _: # conn2
|
||||
_.unbind_ext_s()
|
||||
self.assertEqual(len(ldappool_cm), 3)
|
||||
|
||||
# Then open 3 connections again and make sure size does not grow
|
||||
# over 3
|
||||
with _get_conn() as _: # conn1
|
||||
self.assertEqual(len(ldappool_cm), 1)
|
||||
with _get_conn() as _: # conn2
|
||||
self.assertEqual(len(ldappool_cm), 2)
|
||||
with _get_conn() as _: # conn3
|
||||
_.unbind_ext_s()
|
||||
self.assertEqual(len(ldappool_cm), 3)
|
||||
|
||||
def test_password_change_with_pool(self):
|
||||
old_password = self.user_sna['password']
|
||||
self.cleanup_pools()
|
||||
|
||||
# authenticate so that connection is added to pool before password
|
||||
# change
|
||||
user_ref = self.identity_api.authenticate(
|
||||
context={},
|
||||
user_id=self.user_sna['id'],
|
||||
password=self.user_sna['password'])
|
||||
|
||||
self.user_sna.pop('password')
|
||||
self.user_sna['enabled'] = True
|
||||
self.assertDictEqual(user_ref, self.user_sna)
|
||||
|
||||
new_password = 'new_password'
|
||||
user_ref['password'] = new_password
|
||||
self.identity_api.update_user(user_ref['id'], user_ref)
|
||||
|
||||
# now authenticate again to make sure new password works with
|
||||
# conneciton pool
|
||||
user_ref2 = self.identity_api.authenticate(
|
||||
context={},
|
||||
user_id=self.user_sna['id'],
|
||||
password=new_password)
|
||||
|
||||
user_ref.pop('password')
|
||||
self.assertDictEqual(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
|
||||
# password..so no old bind is maintained in this case.
|
||||
self.assertRaises(AssertionError,
|
||||
self.identity_api.authenticate,
|
||||
context={},
|
||||
user_id=self.user_sna['id'],
|
||||
password=old_password)
|
||||
|
||||
|
||||
class LdapIdentitySqlAssignment(LdapPoolCommonTestMixin,
|
||||
test_backend_ldap.LdapIdentitySqlAssignment,
|
||||
tests.TestCase):
|
||||
'''Executes existing base class 150+ tests with pooled LDAP handler to make
|
||||
sure it works without any error.
|
||||
'''
|
||||
def setUp(self):
|
||||
patcher = mock.patch.object(ldap_core.PooledLDAPHandler, 'Connector',
|
||||
fakeldap.FakeLdapPool)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
super(LdapIdentitySqlAssignment, self).setUp()
|
||||
self.addCleanup(self.cleanup_pools)
|
||||
# storing to local variable to avoid long references
|
||||
self.conn_pools = ldap_core.PooledLDAPHandler.connection_pools
|
||||
# super class loads db fixtures which establishes ldap connection
|
||||
# so adding dummy call to highlight connection pool initialization
|
||||
# as its not that obvious though its not needed here
|
||||
self.identity_api.get_user(self.user_foo['id'])
|
||||
|
||||
def config_files(self):
|
||||
config_files = super(LdapIdentitySqlAssignment, self).config_files()
|
||||
config_files.append(tests.dirs.tests_conf('backend_ldap_pool.conf'))
|
||||
return config_files
|
||||
|
||||
@mock.patch.object(ldap_core, 'utf8_encode')
|
||||
def test_utf8_encoded_is_used_in_pool(self, mocked_method):
|
||||
def side_effect(arg):
|
||||
return arg
|
||||
mocked_method.side_effect = side_effect
|
||||
self.identity_api.get_user(self.user_foo['id'])
|
||||
mocked_method.assert_any_call(CONF.ldap.user)
|
||||
mocked_method.assert_any_call(CONF.ldap.password)
|
|
@ -0,0 +1,207 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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 ldappool
|
||||
import uuid
|
||||
|
||||
from keystone.common.ldap import core as ldap_core
|
||||
from keystone import config
|
||||
from keystone.identity.backends import ldap
|
||||
from keystone import tests
|
||||
from keystone.tests import fakeldap
|
||||
from keystone.tests import test_backend_ldap_pool
|
||||
from keystone.tests import test_ldap_livetest
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin,
|
||||
test_ldap_livetest.LiveLDAPIdentity):
|
||||
"""Executes existing LDAP live test with pooled LDAP handler to make
|
||||
sure it works without any error.
|
||||
|
||||
Also executes common pool specific tests via Mixin class.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(LiveLDAPPoolIdentity, self).setUp()
|
||||
self.addCleanup(self.cleanup_pools)
|
||||
# storing to local variable to avoid long references
|
||||
self.conn_pools = ldap_core.PooledLDAPHandler.connection_pools
|
||||
|
||||
def config_files(self):
|
||||
config_files = super(LiveLDAPPoolIdentity, self).config_files()
|
||||
config_files.append(tests.dirs.
|
||||
tests_conf('backend_pool_liveldap.conf'))
|
||||
return config_files
|
||||
|
||||
def config_overrides(self):
|
||||
super(LiveLDAPPoolIdentity, self).config_overrides()
|
||||
self.config_fixture.config(
|
||||
group='identity',
|
||||
driver='keystone.identity.backends.ldap.Identity')
|
||||
|
||||
def test_assert_connector_used_not_fake_ldap_pool(self):
|
||||
handler = ldap_core._get_connection(CONF.ldap.url, use_pool=True)
|
||||
self.assertNotEqual(type(handler.Connector),
|
||||
type(fakeldap.FakeLdapPool))
|
||||
self.assertEqual(type(handler.Connector),
|
||||
type(ldappool.StateConnector))
|
||||
|
||||
def test_async_search_and_result3(self):
|
||||
self.config_fixture.config(group='ldap', page_size=1)
|
||||
self.test_user_enable_attribute_mask()
|
||||
|
||||
def test_pool_size_expands_correctly(self):
|
||||
|
||||
who = CONF.ldap.user
|
||||
cred = CONF.ldap.password
|
||||
# get related connection manager instance
|
||||
ldappool_cm = self.conn_pools[CONF.ldap.url]
|
||||
|
||||
def _get_conn():
|
||||
return ldappool_cm.connection(who, cred)
|
||||
|
||||
with _get_conn() as c1: # 1
|
||||
self.assertEqual(len(ldappool_cm), 1)
|
||||
self.assertTrue(c1.connected, True)
|
||||
self.assertTrue(c1.active, True)
|
||||
with _get_conn() as c2: # conn2
|
||||
self.assertEqual(len(ldappool_cm), 2)
|
||||
self.assertTrue(c2.connected)
|
||||
self.assertTrue(c2.active)
|
||||
|
||||
self.assertEqual(len(ldappool_cm), 2)
|
||||
# c2 went out of context, its connected but not active
|
||||
self.assertTrue(c2.connected)
|
||||
self.assertFalse(c2.active)
|
||||
with _get_conn() as c3: # conn3
|
||||
self.assertEqual(len(ldappool_cm), 2)
|
||||
self.assertTrue(c3.connected)
|
||||
self.assertTrue(c3.active)
|
||||
self.assertTrue(c3 is c2) # same connection is reused
|
||||
self.assertTrue(c2.active)
|
||||
with _get_conn() as c4: # conn4
|
||||
self.assertEqual(len(ldappool_cm), 3)
|
||||
self.assertTrue(c4.connected)
|
||||
self.assertTrue(c4.active)
|
||||
|
||||
def test_password_change_with_auth_pool_disabled(self):
|
||||
self.config_fixture.config(group='ldap', use_auth_pool=False)
|
||||
old_password = self.user_sna['password']
|
||||
|
||||
self.test_password_change_with_pool()
|
||||
|
||||
self.assertRaises(AssertionError,
|
||||
self.identity_api.authenticate,
|
||||
context={},
|
||||
user_id=self.user_sna['id'],
|
||||
password=old_password)
|
||||
|
||||
def _create_user_and_authenticate(self, password):
|
||||
user_dict = {
|
||||
'domain_id': CONF.identity.default_domain_id,
|
||||
'name': uuid.uuid4().hex,
|
||||
'password': password}
|
||||
user = self.identity_api.create_user(user_dict)
|
||||
|
||||
self.identity_api.authenticate(
|
||||
context={},
|
||||
user_id=user['id'],
|
||||
password=password)
|
||||
|
||||
return self.identity_api.get_user(user['id'])
|
||||
|
||||
def _get_auth_conn_pool_cm(self):
|
||||
pool_url = ldap_core.PooledLDAPHandler.auth_pool_prefix + CONF.ldap.url
|
||||
return self.conn_pools[pool_url]
|
||||
|
||||
def _do_password_change_for_one_user(self, password, new_password):
|
||||
self.config_fixture.config(group='ldap', use_auth_pool=True)
|
||||
self.cleanup_pools()
|
||||
self.load_backends()
|
||||
|
||||
user1 = self._create_user_and_authenticate(password)
|
||||
auth_cm = self._get_auth_conn_pool_cm()
|
||||
self.assertEqual(len(auth_cm), 1)
|
||||
user2 = self._create_user_and_authenticate(password)
|
||||
self.assertEqual(len(auth_cm), 1)
|
||||
user3 = self._create_user_and_authenticate(password)
|
||||
self.assertEqual(len(auth_cm), 1)
|
||||
user4 = self._create_user_and_authenticate(password)
|
||||
self.assertEqual(len(auth_cm), 1)
|
||||
user5 = self._create_user_and_authenticate(password)
|
||||
self.assertEqual(len(auth_cm), 1)
|
||||
|
||||
# connection pool size remains 1 even for different user ldap bind
|
||||
# as there is only one active connection at a time
|
||||
|
||||
user_api = ldap.UserApi(CONF)
|
||||
u1_dn = user_api._id_to_dn_string(user1['id'])
|
||||
u2_dn = user_api._id_to_dn_string(user2['id'])
|
||||
u3_dn = user_api._id_to_dn_string(user3['id'])
|
||||
u4_dn = user_api._id_to_dn_string(user4['id'])
|
||||
u5_dn = user_api._id_to_dn_string(user5['id'])
|
||||
|
||||
# now create multiple active connections for end user auth case which
|
||||
# will force to keep them in pool. After that, modify one of user
|
||||
# password. Need to make sure that user connection is in middle
|
||||
# of pool list.
|
||||
auth_cm = self._get_auth_conn_pool_cm()
|
||||
with auth_cm.connection(u1_dn, password) as _:
|
||||
with auth_cm.connection(u2_dn, password) as _:
|
||||
with auth_cm.connection(u3_dn, password) as _:
|
||||
with auth_cm.connection(u4_dn, password) as _:
|
||||
with auth_cm.connection(u5_dn, password) as _:
|
||||
self.assertEqual(len(auth_cm), 5)
|
||||
_.unbind_s()
|
||||
|
||||
user3['password'] = new_password
|
||||
self.identity_api.update_user(user3['id'], user3)
|
||||
|
||||
return user3
|
||||
|
||||
def test_password_change_with_auth_pool_enabled_long_lifetime(self):
|
||||
self.config_fixture.config(group='ldap',
|
||||
auth_pool_connection_lifetime=600)
|
||||
old_password = 'my_password'
|
||||
new_password = 'new_password'
|
||||
user = self._do_password_change_for_one_user(old_password,
|
||||
new_password)
|
||||
user.pop('password')
|
||||
|
||||
# with long connection lifetime auth_pool can bind to old password
|
||||
# successfully which is not desired if password change is frequent
|
||||
# use case in a deployment.
|
||||
# This can happen in multiple concurrent connections case only.
|
||||
user_ref = self.identity_api.authenticate(
|
||||
context={}, user_id=user['id'], password=old_password)
|
||||
|
||||
self.assertDictEqual(user_ref, user)
|
||||
|
||||
def test_password_change_with_auth_pool_enabled_no_lifetime(self):
|
||||
self.config_fixture.config(group='ldap',
|
||||
auth_pool_connection_lifetime=0)
|
||||
|
||||
old_password = 'my_password'
|
||||
new_password = 'new_password'
|
||||
user = self._do_password_change_for_one_user(old_password,
|
||||
new_password)
|
||||
# now as connection lifetime is zero, so authentication
|
||||
# with old password will always fail.
|
||||
self.assertRaises(AssertionError,
|
||||
self.identity_api.authenticate,
|
||||
context={}, user_id=user['id'],
|
||||
password=old_password)
|
|
@ -16,6 +16,7 @@ pymongo>=2.5
|
|||
# python-ldap does not install on py3
|
||||
# authenticate against an existing LDAP server
|
||||
# python-ldap==2.3.13
|
||||
# ldappool>=1.0
|
||||
|
||||
# Testing
|
||||
# computes code coverage percentages
|
||||
|
|
|
@ -13,6 +13,7 @@ pymongo>=2.5
|
|||
# Optional backend: LDAP
|
||||
# authenticate against an existing LDAP server
|
||||
python-ldap==2.3.13
|
||||
ldappool>=1.0
|
||||
|
||||
# Testing
|
||||
# computes code coverage percentages
|
||||
|
|
Loading…
Reference in New Issue