Repository: django-auth-ldap/django-auth-ldap

Issue:
Allow to raise LDAP errors via settings
I use django-auth-ldap in combination with django-axes, which locks out users after too many failed login attempts. I recently had a situation where the LDAP server was not reachable. Users who tried to log in during that time were locked out by django-axes, which made the situation even worse. Even after the LDAP server was reachable again, many users were still unable to log in.

As far as I understand, a fix would be to use the `ldap_error` signal to raise exceptions if the connection fails:

```python
from django_auth_ldap.backend import ldap_error
from django.dispatch import receiver

@receiver(ldap_error)
def raise_ldap_error(sender, user, request, exception, **kwargs):
    raise exception
```

However, I am using a third party software, so I can only change the settings. If I try to add this signal handler in the settings, I get the following error:

```
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
```

Note that it wouldn't work to add the signal handler to the software itself, because it can be combined with different authentication backends. This handler only makes sense for my specific setup.

So my request is to add a boolean setting (e.g. `AUTH_LDAP_RAISE_LDAP_ERRORS`). I would also be fine with more flexible solutions, as long as they can be used in the settings, without modifying application code.

Discussion:

Patch:
diff --git a/django_auth_ldap/backend.py b/django_auth_ldap/backend.py
index c63b6ce..4356d79 100644
--- a/django_auth_ldap/backend.py
+++ b/django_auth_ldap/backend.py
@@ -51,7 +51,6 @@ import django.conf
 import django.dispatch
 import ldap
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group, Permission
 from django.core.cache import cache
 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
 
@@ -777,6 +776,8 @@ class _LDAPUser:
         Mirrors the user's LDAP groups in the Django database and updates the
         user's membership.
         """
+        from django.contrib.auth.models import Group
+
         try:
             target_group_names = frozenset(self._get_groups().get_group_names())
         except ldap.LDAPError as e:
@@ -833,6 +834,8 @@ class _LDAPUser:
         Populates self._group_permissions based on LDAP group membership and
         Django group permissions.
         """
+        from django.contrib.auth.models import Permission
+
         group_names = self._get_groups().get_group_names()
 
         perms = Permission.objects.filter(group__name__in=group_names)
diff --git a/pyproject.toml b/pyproject.toml
index 3b4a52b..8bb75a0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "django-auth-ldap"
-requires-python = ">=3.8"
+requires-python = ">=3.9"
 description = "Django LDAP authentication backend"
 readme = "README.rst"
 authors = [
@@ -23,7 +23,6 @@ classifiers = [
     "Programming Language :: Python",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3 :: Only",
-    "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
diff --git a/tox.ini b/tox.ini
index 87ffa51..dd4f3f9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,7 +10,6 @@ isolated_build = true
 
 [gh]
 python =
-    3.8 = django42
     3.9 = django42
     3.10 = django{42,50,51,main}
     3.11 = django{42,50,51,main}
@@ -18,7 +17,9 @@ python =
     3.13 = django{42,50,51,main}
 
 [testenv]
-commands = {envpython} -Wa -b -m django test --settings tests.settings
+commands =
+    {envpython} -Wa -b -m django test --settings tests.settings
+    {envpython} -Wa -b -m unittest discover --pattern *_test_without_django.py
 deps =
     django42: Django>=4.2,<4.3
     django50: Django>=5.0,<5.1


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6c71ffd..046b73b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,7 +14,6 @@ jobs:
       matrix:
         # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
         python-version:
-          - '3.8'
           - '3.9'
           - '3.10'
           - '3.11'
diff --git a/tests/import_test_without_django.py b/tests/import_test_without_django.py
new file mode 100644
index 0000000..0fbdc9b
--- /dev/null
+++ b/tests/import_test_without_django.py
@@ -0,0 +1,16 @@
+import os
+from unittest import TestCase
+
+
+class TestLoading(TestCase):
+    def test_django_not_ready(self):
+        orig_env = os.environ.copy()
+
+        def reset_env():
+            os.environ = orig_env
+
+        self.addCleanup(reset_env)
+
+        os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings"
+
+        import django_auth_ldap.backend  # noqa: F401