Repository: privacyidea/privacyidea

Issue:
/auth fails - LDAPServerPoolExhaustedError
An irresponsive LDAP resolver prevents an /auth login and exits with a error message for the irresponsive LDAP server

```python
LDAPServerPoolExhaustedError('no active server available in server pool after maximum number of tries')
```




Discussion:
This issue was not present in 3.3.3.

It occurs for the first time in 3.4dev6. The list of closed issues since 3.4dev5 ist this:
https://github.com/privacyidea/privacyidea/issues?q=is%3Aissue+closed%3A%3E2020-08-22+

The present error must be due to the pre-auth user object assignment which was changed in pull request #2346 
https://github.com/privacyidea/privacyidea/blob/2fc1137f235f3153d35f48e7a4d5e9363fa60c0e/privacyidea/api/auth.py#L115

Resolving patch:
diff --git a/privacyidea/api/auth.py b/privacyidea/api/auth.py
--- a/privacyidea/api/auth.py
+++ b/privacyidea/api/auth.py
@@ -74,6 +74,7 @@
 from privacyidea.lib.event import event, EventConfiguration
 from privacyidea.lib import _
 import logging
+import traceback
 
 log = logging.getLogger(__name__)
 
@@ -112,7 +113,12 @@ def before_request():
         # overwrite the split realm if we have a realm parameter. Default back to default_realm
         realm = getParam(request.all_data, "realm", default=realm) or realm or get_default_realm()
         # Prefill the request.User. This is used by some pre-event handlers
-        request.User = User(loginname, realm)
+        try:
+            request.User = User(loginname, realm)
+        except Exception as e:
+            request.User = None
+            log.warning(u"Problem resolving user {0!s} in realm {1!s}: {2!s}.".format(loginname, realm, e))
+            log.debug(u"{0!s}".format(traceback.format_exc()))
 
 
 @jwtauth.route('', methods=['POST'])


Test patch:
diff --git a/tests/ldap3mock.py b/tests/ldap3mock.py
--- a/tests/ldap3mock.py
+++ b/tests/ldap3mock.py
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 """
+2020-09-07 Cornelius Kölbel <cornelius.koelbel@netknights.it>
+           Add exception
 2017-04-26 Friedrich Weber <friedrich.weber@netknights.it>
            Make it possible to check for correct LDAPS/STARTTLS settings
 2017-01-08 Cornelius Kölbel <cornelius.koelbel@netknights.it>
@@ -658,6 +660,7 @@ def __init__(self):
         self._calls = CallList()
         self._server_mock = None
         self.directory = []
+        self.exception = None
         self.reset()
 
     def reset(self):
@@ -674,6 +677,9 @@ def setLDAPDirectory(self, directory=None):
             except OSError as e:
                 raise
 
+    def set_exception(self, exc=True):
+        self.exception = exc
+
     def _load_data(self, directory):
         try:
             with open(directory, 'r') as f:
@@ -717,6 +723,9 @@ def _on_Connection(self, server, user, password,
         and object
             response
         """
+        # Raise an exception, if we are told to do so
+        if self.exception:
+            raise Exception("LDAP request failed")
         # check the password
         correct_password = False
         # Anonymous bind
diff --git a/tests/test_api_auth.py b/tests/test_api_auth.py
--- a/tests/test_api_auth.py
+++ b/tests/test_api_auth.py
@@ -7,12 +7,13 @@
 from privacyidea.lib.policy import (set_policy, SCOPE, ACTION, REMOTE_USER,
                                     delete_policy)
 from privacyidea.lib.auth import create_db_admin
-from privacyidea.lib.resolver import save_resolver
-from privacyidea.lib.realm import set_realm, set_default_realm
+from privacyidea.lib.resolver import save_resolver, delete_resolver
+from privacyidea.lib.realm import set_realm, set_default_realm, delete_realm
 from privacyidea.lib.event import set_event, delete_event
 from privacyidea.lib.eventhandler.base import CONDITION
 from privacyidea.lib.token import get_tokens, remove_token
 from privacyidea.lib.user import User
+from . import ldap3mock
 
 
 PWFILE = "tests/testdata/passwd-duplicate-name"
@@ -677,3 +678,47 @@ def test_01_setup_eventhandlers(self):
         delete_policy("crhotp")
         delete_event(eid)
         remove_token(hotptoken.token.serial)
+
+    @ldap3mock.activate
+    def test_02_failing_resolver(self):
+        ldap3mock.setLDAPDirectory([])
+        # define, that we want to get an exception
+        ldap3mock.set_exception()
+        # Create an LDAP Realm as default realm
+        params = {'LDAPURI': 'ldap://localhost',
+                  'LDAPBASE': 'o=test',
+                  'BINDDN': 'cn=manager,ou=example,o=test',
+                  'BINDPW': 'ldaptest',
+                  'LOGINNAMEATTRIBUTE': 'cn',
+                  'LDAPSEARCHFILTER': '(cn=*)',
+                  'USERINFO': '{ "username": "cn",'
+                                  '"phone" : "telephoneNumber", '
+                                  '"mobile" : "mobile"'
+                                  ', "email" : "mail", '
+                                  '"surname" : "sn", '
+                                  '"givenname" : "givenName" }',
+                  'UIDTYPE': 'DN',
+                  "resolver": "ldap1",
+                  "type": "ldapresolver"}
+        save_resolver(params)
+        set_realm("ldap1", ["ldap1"])
+        set_default_realm("ldap1")
+
+        # Try to login as internal admin with a failing LDAP resolver
+        with mock.patch("logging.Logger.warning") as mock_log:
+            with self.app.test_request_context('/auth',
+                                               method='POST',
+                                               data={"username": "testadmin",
+                                                     "password": "testpw"}):
+                res = self.app.full_dispatch_request()
+                self.assertEqual(200, res.status_code, res)
+                result = res.json.get("result")
+                self.assertTrue(result.get("status"), result)
+                self.assertIn('token', result.get("value"), result)
+                # role should be 'admin'
+                self.assertEqual('admin', result['value']['role'], result)
+                mock_log.assert_called_once_with("Problem resolving user testadmin in realm ldap1: LDAP request failed.")
+
+        delete_realm("ldap1")
+        delete_resolver("ldap1")
+        ldap3mock.set_exception(False)