Skip to content

Propagate LDAP errors instead of silently ignoring, send more ldap_error signals #379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 70 additions & 37 deletions django_auth_ldap/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,31 @@
ldap_error = django.dispatch.Signal()


_error_context_descriptions = {
"authenticate": "while authenticating",
"populate_user": "populating user info",
"get_group_permissions": "loading group permissions",
"search_for_user_dn": "looking up user",
"mirror_groups": "updating mirrored groups",
}


def _report_error(sender, context, user, request, exception):
description = _error_context_descriptions.get(context, "from unknown context")
logger.warning(
"Caught LDAPError %s: %s",
description,
pprint.pformat(exception)
)
ldap_error.send(
sender,
context=context,
user=user,
request=request,
exception=exception,
)


class LDAPBackend:
"""
The main backend class. This implements the auth backend API, although it
Expand Down Expand Up @@ -354,19 +379,13 @@ def authenticate(self, password):
except self.AuthenticationFailed as e:
logger.debug("Authentication failed for %s: %s", self._username, e)
except ldap.LDAPError as e:
results = ldap_error.send(
_report_error(
type(self.backend),
context="authenticate",
user=self._user,
request=self._request,
exception=e,
"authenticate",
self._user,
self._request,
e
)
if len(results) == 0:
logger.warning(
"Caught LDAPError while authenticating %s: %s",
self._username,
pprint.pformat(e),
)
except Exception as e:
logger.warning("%s while authenticating %s", e, self._username)
raise
Expand All @@ -386,18 +405,13 @@ def get_group_permissions(self):
if self.dn is not None:
self._load_group_permissions()
except ldap.LDAPError as e:
results = ldap_error.send(
_report_error(
type(self.backend),
context="get_group_permissions",
user=self._user,
request=self._request,
exception=e,
"get_group_permissions",
self._user,
self._request,
e
)
if len(results) == 0:
logger.warning(
"Caught LDAPError loading group permissions: %s",
pprint.pformat(e),
)

return self._group_permissions

Expand All @@ -414,20 +428,17 @@ def populate_user(self):
self._get_or_create_user(force_populate=True)

user = self._user
except self.AuthenticationFailed as e:
# Mirroring groups can raise AuthenticationFailed
logger.debug("Failed to populate user %s: %s", self._username, e)
except ldap.LDAPError as e:
results = ldap_error.send(
_report_error(
type(self.backend),
context="populate_user",
user=self._user,
request=self._request,
exception=e,
"populate_user",
self._user,
self._request,
e
)
if len(results) == 0:
logger.warning(
"Caught LDAPError while authenticating %s: %s",
self._username,
pprint.pformat(e),
)
except Exception as e:
logger.warning("%s while authenticating %s", e, self._username)
raise
Expand Down Expand Up @@ -537,11 +548,21 @@ def _search_for_user():
"AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance."
)

results = search.execute(self.connection, {"user": self._username})
if results is not None and len(results) == 1:
(user_dn, self._user_attrs) = next(iter(results))
user_dn = None

try:
results = search.execute(self.connection, {"user": self._username})
except ldap.LDAPError as e:
_report_error(
type(self.backend),
"search_for_user_dn",
self._user,
self._request,
e
)
else:
user_dn = None
if results is not None and len(results) == 1:
(user_dn, self._user_attrs) = next(iter(results))

return user_dn

Expand Down Expand Up @@ -756,7 +777,19 @@ def _mirror_groups(self):
Mirrors the user's LDAP groups in the Django database and updates the
user's membership.
"""
target_group_names = frozenset(self._get_groups().get_group_names())
try:
target_group_names = frozenset(self._get_groups().get_group_names())
except ldap.LDAPError as e:
_report_error(
type(self.backend),
context="mirror_groups",
user=self._user,
request=self._request,
exception=e,
)
# Prevent user from logging in since their groups are out of sync
raise self.AuthenticationFailed("Error mirroring user groups")

current_group_names = frozenset(
self._user.groups.values_list("name", flat=True).iterator()
)
Expand Down
24 changes: 7 additions & 17 deletions django_auth_ldap/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,23 +197,13 @@ def execute(self, connection, filterargs=(), escape=True):
if escape:
filterargs = self._escape_filterargs(filterargs)

try:
filterstr = self.filterstr % filterargs
logger.debug(
"Invoking search_s('%s', %s, '%s')", self.base_dn, self.scope, filterstr
)
results = connection.search_s(
self.base_dn, self.scope, filterstr, self.attrlist
)
except ldap.LDAPError as e:
results = []
logger.error(
"search_s('%s', %s, '%s') raised %s",
self.base_dn,
self.scope,
filterstr,
pprint.pformat(e),
)
filterstr = self.filterstr % filterargs
logger.debug(
"Invoking search_s('%s', %s, '%s')", self.base_dn, self.scope, filterstr
)
results = connection.search_s(
self.base_dn, self.scope, filterstr, self.attrlist
)

return self._process_results(results)

Expand Down
13 changes: 8 additions & 5 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -577,21 +577,24 @@ Backend

.. data:: ldap_error


This is a Django signal that is sent when we receive an
:exc:`ldap.LDAPError` exception. The signal has four keyword arguments:

- ``context``: one of ``'authenticate'``, ``'get_group_permissions'``, or
``'populate_user'``, indicating which API was being called when the
exception was caught.
- ``user``: the Django user being processed (if available).
``'populate_user'``, ``'search_for_user_dn'`` or ``'mirror_groups'``,
indicating which API was being called when the exception was caught.
- ``user``: the Django user being processed (if available) or ``None``.
- ``request``: the Django request object associated with the
authentication attempt (if available).
authentication attempt (if available) or ``None``.
- ``exception``: the :exc:`~ldap.LDAPError` object itself.

The sender is the :class:`~django_auth_ldap.backend.LDAPBackend` class (or
subclass).

By default, LDAP errors are be handled by ``django_auth_ldap`` by failing
the authentication. If instead you wish to propagate the error to up
application code, then raise an exception from the signal handler.

.. class:: LDAPBackend

:class:`~django_auth_ldap.backend.LDAPBackend` has one method that may be
Expand Down
111 changes: 109 additions & 2 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import pickle
from copy import deepcopy
from unittest import mock
from unittest.mock import ANY

import ldap
import slapdtest
Expand Down Expand Up @@ -602,6 +603,31 @@ def test_populate_user_with_missing_attribute(self):
],
)

def test_populate_user_ldap_error(self):
self._init_settings(
USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test",
USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"},
SERVER_URI="<invalid>", # This will cause a network error
)

with self.assertLogs("django_auth_ldap", level=logging.DEBUG) as logs:
with catch_signal(ldap_error) as handler:
LDAPBackend().populate_user('alice')

handler.assert_called_once_with(
signal=ldap_error,
sender=LDAPBackend,
context="populate_user",
user=None,
request=None,
exception=ANY,
)
self.assertEqual(
logs.output[-1],
"WARNING:django_auth_ldap:Caught LDAPError populating user info: "
"LDAPError(0, 'Error')"
)

@mock.patch.object(LDAPSearch, "execute", return_value=None)
def test_populate_user_with_bad_search(self, mock_execute):
self._init_settings(
Expand Down Expand Up @@ -720,11 +746,46 @@ def handle_ldap_error(sender, **kwargs):
request = RequestFactory().get("/")
with self.assertRaises(ldap.LDAPError):
authenticate(request=request, username="alice", password="password")
handler.assert_called_once()
assert handler.mock_calls[0].kwargs['context'] == 'search_for_user_dn'
assert handler.mock_calls[1].kwargs['context'] == 'authenticate'
assert handler.call_count == 2
_args, kwargs = handler.call_args
self.assertEqual(kwargs["context"], "authenticate")
self.assertEqual(kwargs["request"], request)

def test_search_for_user_dn_error(self):
self._init_settings(
USER_DN_TEMPLATE=None,
USER_SEARCH=LDAPSearch("ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=*)"),
USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"},
SERVER_URI="<invalid>", # This will cause a network error
)

request = RequestFactory().get("/")

with self.assertLogs("django_auth_ldap", level=logging.DEBUG) as logs:
with catch_signal(ldap_error) as handler:
authenticate(request=request, username="alice", password="password")

handler.assert_called_once_with(
signal=ldap_error,
sender=LDAPBackend,
context="search_for_user_dn",
user=None,
request=request,
exception=ANY,
)
self.assertEqual(
logs.output[-2],
"WARNING:django_auth_ldap:Caught LDAPError looking up user: "
"LDAPError(0, 'Error')"
)
self.assertEqual(
logs.output[-1],
"DEBUG:django_auth_ldap:Authentication failed for alice: failed "
"to map the username to a DN.",
)

def test_populate_signal_ldap_error(self):
self._init_settings(
BIND_DN="uid=bob,ou=people,o=test",
Expand Down Expand Up @@ -1421,6 +1482,52 @@ def test_group_mirroring_blacklist_noop(self):
set(alice.groups.values_list("name", flat=True)), {"mirror1", "mirror3"}
)

def test_group_mirroring_error(self):
self._init_settings(
USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test",
GROUP_SEARCH=LDAPSearch(
"ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)"
),
GROUP_TYPE=PosixGroupType(),
MIRROR_GROUPS=True,
)

grp = Group.objects.create(name="test_group")
alice = User.objects.create(username="alice")
alice.groups.add(grp)

with self.assertLogs("django_auth_ldap", level=logging.DEBUG) as logs:
with catch_signal(ldap_error) as handler:
with mock.patch(
"django_auth_ldap.backend._LDAPUserGroups.get_group_names",
side_effect=ldap.LDAPError(0, "Error")
):
user = authenticate(username="alice", password="password")

self.assertIsNone(user)

# When there's an error populating groups, preserve old user groups.
self.assertEqual(set(alice.groups.all()), {grp})

handler.assert_called_once_with(
signal=ldap_error,
sender=LDAPBackend,
context="mirror_groups",
user=alice,
request=None,
exception=ANY,
)
self.assertEqual(
logs.output[-2],
"WARNING:django_auth_ldap:Caught LDAPError updating mirrored groups: "
"LDAPError(0, 'Error')"
)
self.assertEqual(
logs.output[-1],
"DEBUG:django_auth_ldap:Authentication failed for alice: Error "
"mirroring user groups"
)

def test_authorize_external_users(self):
self._init_settings(
USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test",
Expand Down Expand Up @@ -1542,7 +1649,7 @@ def test_start_tls(self, mock):
self.assertEqual(log2, "DEBUG:django_auth_ldap:Initiating TLS")
self.assertTrue(
log3.startswith(
"WARNING:django_auth_ldap:Caught LDAPError while authenticating alice: "
"WARNING:django_auth_ldap:Caught LDAPError while authenticating: "
)
)

Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy