Skip to content

Commit 881faf2

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 881faf2

File tree

3 files changed

+85
-1
lines changed

3 files changed

+85
-1
lines changed

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
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

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ 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+
""",
239+
)
231240

232241
def resolve_username(self, username_supplied_by_user):
233242
search_dn = self.lookup_dn_search_user
@@ -305,8 +314,31 @@ def resolve_username(self, username_supplied_by_user):
305314
return (user_dn, response[0]["dn"])
306315

307316
def get_connection(self, userdn, password):
317+
try:
318+
return self._get_real_connection(
319+
userdn, password, self.server_address, self.server_port
320+
)
321+
except (
322+
ldap3.core.exceptions.LDAPSocketOpenError,
323+
ldap3.core.exceptions.LDAPBindError,
324+
ldap3.core.exceptions.LDAPSocketReceiveError,
325+
):
326+
for server, port in self._get_secondary_servers():
327+
try:
328+
return self._get_real_connection(userdn, password, server, port)
329+
except (
330+
ldap3.core.exceptions.LDAPSocketOpenError,
331+
ldap3.core.exceptions.LDAPBindError,
332+
ldap3.core.exceptions.LDAPSocketReceiveError,
333+
):
334+
continue
335+
else:
336+
# re-raise the last caught error
337+
raise
338+
339+
def _get_real_connection(self, userdn, password, server_address, server_port):
308340
server = ldap3.Server(
309-
self.server_address, port=self.server_port, use_ssl=self.use_ssl
341+
server_address, port=server_port, use_ssl=self.use_ssl
310342
)
311343
auto_bind = (
312344
ldap3.AUTO_BIND_NO_TLS if self.use_ssl else ldap3.AUTO_BIND_TLS_BEFORE_BIND
@@ -316,6 +348,24 @@ def get_connection(self, userdn, password):
316348
)
317349
return conn
318350

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

ldapauthenticator/tests/test_ldapauthenticator.py

Lines changed: 33 additions & 0 deletions
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)