Set connection timeout for LDAP configuration

Presently the Identity LDAP driver does not set a connection timeout
option which has the disadvantage of causing the Identity LDAP backend
handler to stall indefinitely (or until TCP timeout) on LDAP bind, if
a) the LDAP URL is incorrect, or b) there is a connection failure/link
loss.

This commit add a new option to set the LDAP connection timeout to
set a new OPT_NETWORK_TIMEOUT option on the LDAP object. This will
raise ldap.SERVER_DOWN exceptions on timeout.

Signed-off-by: Kam Nasim <kam.nasim@windriver.com>

Closes-Bug: #1636950
Change-Id: I574e6368169ad60bef2cc990d2d410a638d1b770
This commit is contained in:
Kam Nasim 2017-01-11 18:55:40 +00:00
parent 7ada740e1d
commit 2d239cfbc3
6 changed files with 137 additions and 39 deletions

View File

@ -483,6 +483,15 @@ always be requested but not required from the LDAP server. If set to `never`,
then a certificate will never be requested.
"""))
connection_timeout = cfg.IntOpt(
'connection_timeout',
default=-1,
min=-1,
help=utils.fmt("""
The connection timeout to use with the LDAP server. A value of `-1` means that
connections will never timeout.
"""))
use_pool = cfg.BoolOpt(
'use_pool',
default=True,
@ -523,9 +532,9 @@ pool_connection_timeout = cfg.IntOpt(
default=-1,
min=-1,
help=utils.fmt("""
The connection timeout to use with the LDAP server. A value of `-1` means that
connections will never timeout. This option has no effect unless `[ldap]
use_pool` is also enabled.
The connection timeout to use when pooling LDAP connections. A value of `-1`
means that connections will never timeout. This option has no effect unless
`[ldap] use_pool` is also enabled.
"""))
pool_connection_lifetime = cfg.IntOpt(
@ -621,6 +630,7 @@ ALL_OPTS = [
tls_cacertdir,
use_tls,
tls_req_cert,
connection_timeout,
use_pool,
pool_size,
pool_retry_max,

View File

@ -571,3 +571,10 @@ class TokenlessAuthConfigError(ValidationError):
class CredentialEncryptionError(Exception):
message_format = _("An unexpected error prevented the server "
"from accessing encrypted credentials.")
class LDAPServerConnectionError(Error):
message_format = _('Timed out waiting to establish a '
'connection to the LDAP Server (%(url)s).')
code = 504
title = 'Gateway Timeout'

View File

@ -428,8 +428,8 @@ class LDAPHandler(object):
def connect(self, url, page_size=0, alias_dereferencing=None,
use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
tls_req_cert=ldap.OPT_X_TLS_DEMAND, chase_referrals=None,
debug_level=None, use_pool=None, pool_size=None,
pool_retry_max=None, pool_retry_delay=None,
debug_level=None, conn_timeout=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
@ -496,8 +496,8 @@ 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=ldap.OPT_X_TLS_DEMAND, chase_referrals=None,
debug_level=None, use_pool=None, pool_size=None,
pool_retry_max=None, pool_retry_delay=None,
debug_level=None, conn_timeout=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,
@ -505,7 +505,8 @@ class PythonLDAPHandler(LDAPHandler):
tls_cacertfile=tls_cacertfile,
tls_cacertdir=tls_cacertdir,
tls_req_cert=tls_req_cert,
debug_level=debug_level)
debug_level=debug_level,
timeout=conn_timeout)
self.conn = ldap.initialize(url)
self.conn.protocol_version = ldap.VERSION3
@ -569,7 +570,7 @@ class PythonLDAPHandler(LDAPHandler):
def _common_ldap_initialization(url, use_tls=False, tls_cacertfile=None,
tls_cacertdir=None, tls_req_cert=None,
debug_level=None):
debug_level=None, timeout=None):
"""LDAP initialization for PythonLDAPHandler and PooledLDAPHandler."""
LOG.debug('LDAP init: url=%s', url)
LOG.debug('LDAP init: use_tls=%s tls_cacertfile=%s tls_cacertdir=%s '
@ -582,6 +583,10 @@ def _common_ldap_initialization(url, use_tls=False, tls_cacertfile=None,
using_ldaps = url.lower().startswith("ldaps")
if timeout is not None and timeout > 0:
# set network connection timeout
ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, timeout)
if use_tls and using_ldaps:
raise AssertionError(_('Invalid TLS / LDAPS combination'))
@ -683,8 +688,8 @@ class PooledLDAPHandler(LDAPHandler):
def connect(self, url, page_size=0, alias_dereferencing=None,
use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
tls_req_cert=ldap.OPT_X_TLS_DEMAND, chase_referrals=None,
debug_level=None, use_pool=None, pool_size=None,
pool_retry_max=None, pool_retry_delay=None,
debug_level=None, conn_timeout=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,
@ -692,7 +697,8 @@ class PooledLDAPHandler(LDAPHandler):
tls_cacertfile=tls_cacertfile,
tls_cacertdir=tls_cacertdir,
tls_req_cert=tls_req_cert,
debug_level=debug_level)
debug_level=debug_level,
timeout=pool_conn_timeout)
self.page_size = page_size
@ -873,14 +879,15 @@ 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=ldap.OPT_X_TLS_DEMAND, chase_referrals=None,
debug_level=None, use_pool=None, pool_size=None,
pool_retry_max=None, pool_retry_delay=None,
debug_level=None, conn_timeout=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,
conn_timeout=conn_timeout,
use_pool=use_pool,
pool_size=pool_size,
pool_retry_max=pool_retry_max,
@ -1147,6 +1154,7 @@ class BaseLdap(object):
self.attribute_mapping = {}
self.chase_referrals = conf.ldap.chase_referrals
self.debug_level = conf.ldap.debug_level
self.conn_timeout = conf.ldap.connection_timeout
# LDAP Pool specific attribute
self.use_pool = conf.ldap.use_pool
@ -1259,34 +1267,42 @@ class BaseLdap(object):
conn = KeystoneLDAPHandler(conn=conn)
conn.connect(self.LDAP_URL,
page_size=self.page_size,
alias_dereferencing=self.alias_dereferencing,
use_tls=self.use_tls,
tls_cacertfile=self.tls_cacertfile,
tls_cacertdir=self.tls_cacertdir,
tls_req_cert=self.tls_req_cert,
chase_referrals=self.chase_referrals,
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)
# The LDAP server may be down or a connection may not
# exist. If that is the case, the bind attempt will
# fail with a server down exception.
try:
conn.connect(self.LDAP_URL,
page_size=self.page_size,
alias_dereferencing=self.alias_dereferencing,
use_tls=self.use_tls,
tls_cacertfile=self.tls_cacertfile,
tls_cacertdir=self.tls_cacertdir,
tls_req_cert=self.tls_req_cert,
chase_referrals=self.chase_referrals,
debug_level=self.debug_level,
conn_timeout=self.conn_timeout,
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
if user is None:
user = self.LDAP_USER
if password is None:
password = self.LDAP_PASSWORD
if password is None:
password = self.LDAP_PASSWORD
# not all LDAP servers require authentication, so we don't bind
# if we don't have any user/pass
if user and password:
conn.simple_bind_s(user, password)
# not all LDAP servers require authentication, so we don't bind
# if we don't have any user/pass
if user and password:
conn.simple_bind_s(user, password)
return conn
return conn
except ldap.SERVER_DOWN:
raise exception.LDAPServerConnectionError(
url=self.LDAP_URL)
def _id_to_dn_string(self, object_id):
return u'%s=%s,%s' % (self.id_attr,

View File

@ -244,7 +244,8 @@ class FakeLdap(common.LDAPHandler):
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):
pool_conn_lifetime=None,
conn_timeout=None):
if url.startswith('fake://memory'):
if url not in FakeShelves:
FakeShelves[url] = FakeShelve()
@ -278,6 +279,7 @@ class FakeLdap(common.LDAPHandler):
self.pool_retry_delay = pool_retry_delay
self.pool_conn_timeout = pool_conn_timeout
self.pool_conn_lifetime = pool_conn_lifetime
self.conn_timeout = conn_timeout
def dn(self, dn):
return common.utf8_decode(dn)

View File

@ -298,6 +298,59 @@ class MultiURLTests(unit.TestCase):
self.assertEqual(urls, ldap_connection.conn.conn_pool.uri)
class LDAPConnectionTimeoutTest(unit.TestCase):
"""Test for Network Connection timeout on LDAP URL connection."""
def test_connectivity_timeout_no_conn_pool(self):
url = 'ldap://localhost'
conn_timeout = 1 # 1 second
self.config_fixture.config(group='ldap',
url=url,
connection_timeout=conn_timeout,
use_pool=False)
base_ldap = common_ldap.BaseLdap(CONF)
ldap_connection = base_ldap.get_connection()
self.assertIsInstance(ldap_connection.conn,
common_ldap.PythonLDAPHandler)
# Ensure that the Network Timeout option is set.
# Also ensure that the URL is set.
#
# We will not verify if an LDAP bind returns the timeout
# exception as that would fall under the realm of
# integration testing. If the LDAP option is set properly,
# and we get back a valid connection URI then that should
# suffice for this unit test.
self.assertEqual(conn_timeout,
ldap.get_option(ldap.OPT_NETWORK_TIMEOUT))
self.assertEqual(url, ldap_connection.conn.conn._uri)
def test_connectivity_timeout_with_conn_pool(self):
url = 'ldap://localhost'
conn_timeout = 1 # 1 second
self.config_fixture.config(group='ldap',
url=url,
pool_connection_timeout=conn_timeout,
use_pool=True,
pool_retry_max=1)
base_ldap = common_ldap.BaseLdap(CONF)
ldap_connection = base_ldap.get_connection()
self.assertIsInstance(ldap_connection.conn,
common_ldap.PooledLDAPHandler)
# Ensure that the Network Timeout option is set.
# Also ensure that the URL is set.
#
# We will not verify if an LDAP bind returns the timeout
# exception as that would fall under the realm of
# integration testing. If the LDAP option is set properly,
# and we get back a valid connection URI then that should
# suffice for this unit test.
self.assertEqual(conn_timeout,
ldap.get_option(ldap.OPT_NETWORK_TIMEOUT))
self.assertEqual(url, ldap_connection.conn.conn_pool.uri)
class SslTlsTest(unit.BaseTestCase):
"""Test for the SSL/TLS functionality in keystone.common.ldap.core."""

View File

@ -0,0 +1,10 @@
---
fixes:
- >
[`bug 1636950 <https://bugs.launchpad.net/keystone/+bug/1636950>`_]
New option ``[ldap] connection_timeout`` allows a deployer to set a
``OPT_NETWORK_TIMEOUT`` value to use with the LDAP server.
This allows the LDAP server to return a ``SERVER_DOWN`` exception,
if the LDAP URL is incorrect if there is a connection failure. By default,
the value for ``[ldap] connection_timeout`` is -1, meaning it is disabled.
Set a postive value (in seconds) to enable the option.