Skip to content

Commit 99dad8c

Browse files
author
Kota Tsuyuzaki
committed
Add secondary_uri option to allow ldap server redirect
When a server set in server_address is unavailable, currently no way to try to connect other available servers even system provides HA ldap servers. This patch allows users to set such HA servers as secondary_uri, then, ldap client will access to them if the primary is not available.
1 parent 2c4b027 commit 99dad8c

File tree

3 files changed

+84
-1
lines changed

3 files changed

+84
-1
lines changed

dev-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ html5lib # needed for beautifulsoup
66
mock
77
notebook
88
pre-commit
9+
psutil
910
pytest-asyncio
1011
pytest-cov
1112
pytest>=3.3

ldapauthenticator/ldapauthenticator.py

+50-1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,14 @@ def _server_port_default(self):
228228
This can be useful in an heterogeneous environment, when supplying a UNIX username to authenticate against AD.
229229
""",
230230
)
231+
secondary_uri = Unicode(
232+
config=True,
233+
default="",
234+
help="""
235+
Comma separated address:port of the LDAP server which can be tried to contact when
236+
primary LDAP server is unavailable.
237+
""",
238+
)
231239

232240
def resolve_username(self, username_supplied_by_user):
233241
search_dn = self.lookup_dn_search_user
@@ -305,8 +313,31 @@ def resolve_username(self, username_supplied_by_user):
305313
return (user_dn, response[0]["dn"])
306314

307315
def get_connection(self, userdn, password):
316+
try:
317+
return self._get_real_connection(
318+
userdn, password, self.server_address, self.server_port
319+
)
320+
except (
321+
ldap3.core.exceptions.LDAPSocketOpenError,
322+
ldap3.core.exceptions.LDAPBindError,
323+
ldap3.core.exceptions.LDAPSocketReceiveError,
324+
):
325+
for server, port in self._get_secondary_servers():
326+
try:
327+
return self._get_real_connection(userdn, password, server, port)
328+
except (
329+
ldap3.core.exceptions.LDAPSocketOpenError,
330+
ldap3.core.exceptions.LDAPBindError,
331+
ldap3.core.exceptions.LDAPSocketReceiveError,
332+
):
333+
continue
334+
else:
335+
# re-raise the last caught error
336+
raise
337+
338+
def _get_real_connection(self, userdn, password, server_address, server_port):
308339
server = ldap3.Server(
309-
self.server_address, port=self.server_port, use_ssl=self.use_ssl
340+
server_address, port=server_port, use_ssl=self.use_ssl
310341
)
311342
auto_bind = (
312343
ldap3.AUTO_BIND_NO_TLS if self.use_ssl else ldap3.AUTO_BIND_TLS_BEFORE_BIND
@@ -316,6 +347,24 @@ def get_connection(self, userdn, password):
316347
)
317348
return conn
318349

350+
def _get_secondary_servers(self):
351+
uri_list = self.secondary_uri.split(",")
352+
for uri in uri_list:
353+
server_port = uri.strip().split(":")
354+
assert len(server_port) <= 2
355+
if len(server_port) == 2:
356+
try:
357+
port = int(server_port[1])
358+
except ValueError:
359+
self.log.warning(
360+
"Invalid port in secondary uri %s, use default" % uri
361+
)
362+
port = self._server_port_default()
363+
else:
364+
port = self._server_port_default()
365+
366+
yield (server_port[0], port)
367+
319368
def get_user_attributes(self, conn, userdn):
320369
attrs = {}
321370
if self.auth_state_attributes:

ldapauthenticator/tests/test_ldapauthenticator.py

+33
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
# Inspired by https://github.com/jupyterhub/jupyterhub/blob/master/jupyterhub/tests/test_auth.py
2+
import random
3+
4+
import psutil
5+
6+
7+
def unused_port():
8+
while True:
9+
port = random.randint(1024, 65534)
10+
if port not in psutil.net_connections():
11+
return port
212

313

414
async def test_ldap_auth_allowed(authenticator):
@@ -100,3 +110,26 @@ async def test_ldap_auth_state_attributes(authenticator):
100110
)
101111
assert authorized["name"] == "fry"
102112
assert authorized["auth_state"] == {"employeeType": ["Delivery boy"]}
113+
114+
115+
async def test_ldap_auth_redirects(authenticator):
116+
# set non-available port
117+
correct_server_port = "%s:%s" % (
118+
authenticator.server_address,
119+
authenticator._server_port_default(),
120+
)
121+
authenticator.server_port = unused_port()
122+
123+
async def _test_ldap_redirect(uri_pattern):
124+
authenticator.secondary_uri = uri_pattern
125+
authorized = await authenticator.get_authenticated_user(
126+
None, {"username": "fry", "password": "fry"}
127+
)
128+
assert authorized["name"] == "fry"
129+
130+
await _test_ldap_redirect(correct_server_port)
131+
await _test_ldap_redirect("unavailable,%s" % correct_server_port)
132+
await _test_ldap_redirect("unavailable, %s" % correct_server_port)
133+
await _test_ldap_redirect(
134+
"unavailable:8080,localhost:8080,%s" % correct_server_port
135+
)

0 commit comments

Comments
 (0)