Repository: privacyidea/privacyidea

Issue:
 LDAP Resolver: LDAPSizeLimitExceededResult error when listing users with newer ldap3
Steps to reproduce:
* In a current (d006f7e) development environment, notice that ``ldap3==2.1.1`` is installed.
* Create a new LDAP resolver. Click ``Quick Resolver Test``. `` Your LDAP config seems to be OK, 1 user objects found.`` is displayed.
* Update ``ldap3``: ``pip install ldap3==2.3``
* In the same resolver, click ``Quick Resolver Test``
* We get the error `` LDAPSizeLimitExceededResult - 4 - sizeLimitExceeded - None - None - searchResDone - None``

If the configured size limit (500) is less than the count of users, the "User" view does not list any users at all.

This affects, e.g., the CentOS 7 package, as it comes with ldap3 2.3.

**Workaround**: Set a size limit greater than the number of users.
 LDAP Resolver: LDAPSizeLimitExceededResult error when listing users with newer ldap3
Steps to reproduce:
* In a current (d006f7e) development environment, notice that ``ldap3==2.1.1`` is installed.
* Create a new LDAP resolver. Click ``Quick Resolver Test``. `` Your LDAP config seems to be OK, 1 user objects found.`` is displayed.
* Update ``ldap3``: ``pip install ldap3==2.3``
* In the same resolver, click ``Quick Resolver Test``
* We get the error `` LDAPSizeLimitExceededResult - 4 - sizeLimitExceeded - None - None - searchResDone - None``

If the configured size limit (500) is less than the count of users, the "User" view does not list any users at all.

This affects, e.g., the CentOS 7 package, as it comes with ldap3 2.3.

**Workaround**: Set a size limit greater than the number of users.


Discussion:
I tried to pinpoint the issue a bit: On a privacyIDEA 2.21.1 (CentOS 7), I first manually downgraded pyasn1 to ``0.1.9``.
If I then manually install ``ldap3==2.3``, the issue still remains:
```
# pip freeze | grep -e pyasn1 -e ldap3
ldap3==2.3
pyasn1==0.1.9
```
But if I then manually downgrade to ``ldap==2.2.4``, the issue goes away!
```
# pip freeze | grep -e pyasn1 -e ldap3
ldap3==2.2.4
pyasn1==0.1.9
```

So there seems to be some problem with the ldap3 2.2.4 -> 2.3.0 step.
This exception was introduced by cannatag/ldap3@78ff0f86. By its description, it sounds like a feature :-) 
**moved to #911**

~~On a related note, I just stumbled upon another regression involving user info :-/
Apparently, the user info is not fetched properly if using ldap3 2.4. The exact same configuration works with ldap3 2.3.~~
```
╰─$ pip freeze | grep ldap3                                                                                                                                                             
ldap3==2.4
╰─$ http --verify=no 'http://localhost:5000/validate/check' user=user111 pass=test | jq .detail.user
{
  "password": ""
}
╰─$ pip install ldap3==2.3                                                                          
Collecting ldap3==2.3
  Using cached ldap3-2.3-py2.py3-none-any.whl
Requirement already satisfied: pyasn1>=0.1.8 in ./venv/lib/python2.7/site-packages (from ldap3==2.3)
Installing collected packages: ldap3
  Found existing installation: ldap3 2.4
    Uninstalling ldap3-2.4:
      Successfully uninstalled ldap3-2.4
Successfully installed ldap3-2.3
╰─$ http --verify=no 'http://localhost:5000/validate/check' user=user111 pass=test | jq .detail.user
{
  "username": "user111",
  "password": "",
  "surname": "User111"
}
```
**moved to #911**
~~With the new ldap3 2.4.1, we get the following exception:~~
```
$ http 'http://localhost:5000/validate/check' user=user085 pass=test
HTTP/1.0 500 INTERNAL SERVER ERROR
Content-Length: 905
Content-Type: application/json
Date: Tue, 23 Jan 2018 15:32:12 GMT
Server: Werkzeug/0.10.4 Python/2.7.14

{
    "detail": null,
    "id": 1,
    "jsonrpc": "2.0",
    "result": {
        "error": {
            "code": -500,
            "message": "Unable to convert client data to unicode: \"P\\xd7\\xb1Z\\xd3\\r\\x106\\x9fp\\xcdQ'\\xce\\x8b;\""
        },
        "status": false
    },
    "signature": "22592380035439953281395352749925036854485067466597898483180085759564196872784650482781052997700601822367954183150970970339477893948865263388595314450151889369637487724938312434263564979935818647657782967287913858434588245069091583859255225955199127282792706730813958294920042479343676278651898693572502100864778833803258169761353317492121337434511915894699380628487330142487831794321563108530893596782053719457835865651278205510720293898223573964562500467433333031080345954921761262640973108839973304822516443451925357055469141400732872035443003825783798819560599481694680716638551927611830148692400071580952871085278",
    "time": 1516721532.149777,
    "version": "privacyIDEA 2.22.dev2"
}
$ pip freeze| grep ldap3  
ldap3==2.4.1
```

log:
```
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/token.py", line 1909, in check_user_pass
    user=tokenobject.user,
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/tokenclass.py", line 209, in user
    username = get_username(self.token.user_id, self.token.resolver)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/log.py", line 154, in log_wrapper
    return func(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/usercache.py", line 63, in cache_wrapper
    return wrapped_function(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/user.py", line 692, in get_username
    username = y.getUsername(userid)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/resolvers/LDAPIdResolver.py", line 464, in getUsername
    info = self.getUserInfo(user_id)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/resolvers/LDAPIdResolver.py", line 168, in cache_wrapper
    f_result = func(self, *args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/resolvers/LDAPIdResolver.py", line 418, in getUserInfo
    attributes=self.userinfo.values())
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/core/connection.py", line 775, in search
    response = self.post_send_search(self.send('searchRequest', request, controls))
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/strategy/base.py", line 303, in send
    self.connection.request = BaseStrategy.decode_request(message_type, request, controls)
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/strategy/base.py", line 635, in decode_request
    result = search_request_to_dict(component)
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/operation/search.py", line 516, in search_request_to_dict
    'filter': filter_to_string(request['filter']),
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/operation/search.py", line 466, in filter_to_string
    filter_string += filter_to_string(f)
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/operation/search.py", line 474, in filter_to_string
    ava = ava_to_dict(filter_object['equalityMatch'])
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/protocol/convert.py", line 87, in ava_to_dict
    return {'attribute': str(ava['attributeDesc']), 'value': escape_filter_chars(str(ava['assertionValue']))}
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/utils/conv.py", line 91, in escape_filter_chars
    text = to_unicode(text, encoding)
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/utils/conv.py", line 64, in to_unicode
    raise UnicodeError("Unable to convert client data to unicode: %r" % obj)
UnicodeError: Unable to convert client data to unicode: "P\xd7\xb1Z\xd3\r\x106\x9fp\xcdQ'\xce\x8b;"
```
I tried to pinpoint the issue a bit: On a privacyIDEA 2.21.1 (CentOS 7), I first manually downgraded pyasn1 to ``0.1.9``.
If I then manually install ``ldap3==2.3``, the issue still remains:
```
# pip freeze | grep -e pyasn1 -e ldap3
ldap3==2.3
pyasn1==0.1.9
```
But if I then manually downgrade to ``ldap==2.2.4``, the issue goes away!
```
# pip freeze | grep -e pyasn1 -e ldap3
ldap3==2.2.4
pyasn1==0.1.9
```

So there seems to be some problem with the ldap3 2.2.4 -> 2.3.0 step.
This exception was introduced by cannatag/ldap3@78ff0f86. By its description, it sounds like a feature :-) 
**moved to #911**

~~On a related note, I just stumbled upon another regression involving user info :-/
Apparently, the user info is not fetched properly if using ldap3 2.4. The exact same configuration works with ldap3 2.3.~~
```
╰─$ pip freeze | grep ldap3                                                                                                                                                             
ldap3==2.4
╰─$ http --verify=no 'http://localhost:5000/validate/check' user=user111 pass=test | jq .detail.user
{
  "password": ""
}
╰─$ pip install ldap3==2.3                                                                          
Collecting ldap3==2.3
  Using cached ldap3-2.3-py2.py3-none-any.whl
Requirement already satisfied: pyasn1>=0.1.8 in ./venv/lib/python2.7/site-packages (from ldap3==2.3)
Installing collected packages: ldap3
  Found existing installation: ldap3 2.4
    Uninstalling ldap3-2.4:
      Successfully uninstalled ldap3-2.4
Successfully installed ldap3-2.3
╰─$ http --verify=no 'http://localhost:5000/validate/check' user=user111 pass=test | jq .detail.user
{
  "username": "user111",
  "password": "",
  "surname": "User111"
}
```
**moved to #911**
~~With the new ldap3 2.4.1, we get the following exception:~~
```
$ http 'http://localhost:5000/validate/check' user=user085 pass=test
HTTP/1.0 500 INTERNAL SERVER ERROR
Content-Length: 905
Content-Type: application/json
Date: Tue, 23 Jan 2018 15:32:12 GMT
Server: Werkzeug/0.10.4 Python/2.7.14

{
    "detail": null,
    "id": 1,
    "jsonrpc": "2.0",
    "result": {
        "error": {
            "code": -500,
            "message": "Unable to convert client data to unicode: \"P\\xd7\\xb1Z\\xd3\\r\\x106\\x9fp\\xcdQ'\\xce\\x8b;\""
        },
        "status": false
    },
    "signature": "22592380035439953281395352749925036854485067466597898483180085759564196872784650482781052997700601822367954183150970970339477893948865263388595314450151889369637487724938312434263564979935818647657782967287913858434588245069091583859255225955199127282792706730813958294920042479343676278651898693572502100864778833803258169761353317492121337434511915894699380628487330142487831794321563108530893596782053719457835865651278205510720293898223573964562500467433333031080345954921761262640973108839973304822516443451925357055469141400732872035443003825783798819560599481694680716638551927611830148692400071580952871085278",
    "time": 1516721532.149777,
    "version": "privacyIDEA 2.22.dev2"
}
$ pip freeze| grep ldap3  
ldap3==2.4.1
```

log:
```
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/token.py", line 1909, in check_user_pass
    user=tokenobject.user,
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/tokenclass.py", line 209, in user
    username = get_username(self.token.user_id, self.token.resolver)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/log.py", line 154, in log_wrapper
    return func(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/usercache.py", line 63, in cache_wrapper
    return wrapped_function(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/user.py", line 692, in get_username
    username = y.getUsername(userid)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/resolvers/LDAPIdResolver.py", line 464, in getUsername
    info = self.getUserInfo(user_id)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/resolvers/LDAPIdResolver.py", line 168, in cache_wrapper
    f_result = func(self, *args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/resolvers/LDAPIdResolver.py", line 418, in getUserInfo
    attributes=self.userinfo.values())
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/core/connection.py", line 775, in search
    response = self.post_send_search(self.send('searchRequest', request, controls))
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/strategy/base.py", line 303, in send
    self.connection.request = BaseStrategy.decode_request(message_type, request, controls)
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/strategy/base.py", line 635, in decode_request
    result = search_request_to_dict(component)
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/operation/search.py", line 516, in search_request_to_dict
    'filter': filter_to_string(request['filter']),
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/operation/search.py", line 466, in filter_to_string
    filter_string += filter_to_string(f)
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/operation/search.py", line 474, in filter_to_string
    ava = ava_to_dict(filter_object['equalityMatch'])
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/protocol/convert.py", line 87, in ava_to_dict
    return {'attribute': str(ava['attributeDesc']), 'value': escape_filter_chars(str(ava['assertionValue']))}
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/utils/conv.py", line 91, in escape_filter_chars
    text = to_unicode(text, encoding)
  File "/home/fred/privacyidea/privacyidea/venv/lib/python2.7/site-packages/ldap3/utils/conv.py", line 64, in to_unicode
    raise UnicodeError("Unable to convert client data to unicode: %r" % obj)
UnicodeError: Unable to convert client data to unicode: "P\xd7\xb1Z\xd3\r\x106\x9fp\xcdQ'\xce\x8b;"
```

Resolving patch:
diff --git a/privacyidea/lib/resolvers/LDAPIdResolver.py b/privacyidea/lib/resolvers/LDAPIdResolver.py
--- a/privacyidea/lib/resolvers/LDAPIdResolver.py
+++ b/privacyidea/lib/resolvers/LDAPIdResolver.py
@@ -63,6 +63,8 @@
 import ldap3
 from ldap3 import MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE
 from ldap3 import Server, Tls, Connection
+from ldap3.core.exceptions import LDAPOperationResult
+from ldap3.core.results import RESULT_SIZE_LIMIT_EXCEEDED
 import ssl
 
 import os.path
@@ -140,6 +142,44 @@ def get_info_configuration(noschemas):
     return get_schema_info
 
 
+def ignore_sizelimit_exception(conn, generator):
+    """
+    Wrapper for ``paged_search``, which (since ldap3 2.3) throws an exception if the size limit has been
+    reached. This function wraps the generator and ignores this exception.
+
+    Additionally, this checks ``conn.response`` for any leftover entries that were not yet returned
+    by the generator and yields them.
+    """
+    last_entry = None
+    while True:
+        try:
+            last_entry = next(generator)
+            yield last_entry
+        except StopIteration:
+            # If the generator is exceed, we stop
+            break
+        except LDAPOperationResult as e:
+            # If the size limit has been reached, we stop. All other exceptions are re-raised.
+            if e.result == RESULT_SIZE_LIMIT_EXCEEDED:
+                # Workaround: In ldap3 <= 2.4.1, the generator may "forget" to yield some entries that
+                # were transmitted just before the "size limit exceeded" message. In other words,
+                # the exception is raised *before* the generator has yielded those entries.
+                # These leftover entries can still be found in ``conn.response``, so we
+                # just yield them here.
+                # However, as future versions of ldap3 may fix this behavior and
+                # may actually yield those elements as well, this workaround may result in
+                # duplicate entries.
+                # Thus, we check if the last entry we got from the generator can be found
+                # in ``conn.response``. If that is the case, we assume the generator works correctly
+                # and *all* of ``conn.response`` have been yielded already.
+                if last_entry is None or last_entry not in conn.response:
+                    for entry in conn.response:
+                        yield entry
+                break
+            else:
+                raise
+
+
 def cache(func):
     """
     cache the user with his loginname, resolver and UID in a local 
@@ -319,6 +359,14 @@ def _get_uid(entry, uidtype):
                 uid = attributes.get(uidtype)[0]
             else:
                 uid = attributes.get(uidtype)
+            if uidtype.lower() == "objectguid":
+                # For ldap3 versions <= 2.4.1, objectGUID attribute values are returned as UUID strings.
+                # For versions greater than 2.4.1, they are returned in the curly-braced string
+                # representation, i.e. objectGUID := "{" UUID "}"
+                # In order to ensure backwards compatibility for user mappings,
+                # we strip the curly braces from objectGUID values.
+                # If we are using ldap3 <= 2.4.1, there are no curly braces and we leave the value unchanged.
+                uid = uid.strip("{").strip("}")
         return uid
 
     def _trim_user_id(self, userId):
@@ -551,7 +599,7 @@ def getUserList(self, searchDict):
                                                 size_limit=self.sizelimit,
                                                 generator=True)
         # returns a generator of dictionaries
-        for entry in g:
+        for entry in ignore_sizelimit_exception(self.l, g):
             # Simple fix for ignored sizelimit with Active Directory
             if len(ret) >= self.sizelimit:
                 break
@@ -846,7 +894,7 @@ def testconnection(cls, param):
             # returns a generator of dictionaries
             count = 0
             uidtype_count = 0
-            for entry in g:
+            for entry in ignore_sizelimit_exception(l, g):
                 try:
                     userid = cls._get_uid(entry, uidtype)
                     count += 1


Test patch:
diff --git a/tests/ldap3mock.py b/tests/ldap3mock.py
--- a/tests/ldap3mock.py
+++ b/tests/ldap3mock.py
@@ -135,7 +135,11 @@ def paged_search(self, **kwargs):
                                        paged_size=kwargs.get("page_size"),
                                        size_limit=kwargs.get("size_limit"),
                                        paged_cookie=None)
-                return self.connection.response
+                result = self.connection.response
+                if kwargs.get("generator", False):
+                    # If ``generator=True`` is passed, ``paged_search`` should return an iterator.
+                    result = iter(result)
+                return result
 
         def __init__(self, connection):
             self.standard = self.Standard(connection)
diff --git a/tests/test_lib_resolver.py b/tests/test_lib_resolver.py
--- a/tests/test_lib_resolver.py
+++ b/tests/test_lib_resolver.py
@@ -11,6 +11,8 @@
 PWFILE = "tests/testdata/passwords"
 from .base import MyTestCase
 import ldap3mock
+from ldap3.core.exceptions import LDAPOperationResult
+from ldap3.core.results import RESULT_SIZE_LIMIT_EXCEEDED
 import mock
 import responses
 import uuid
@@ -102,7 +104,30 @@
                                 "objectGUID": objectGUIDs[1],
                                 'oid': "1"}}
                        ]
-
+# Same as above, but with curly-braced string representation of objectGUID
+# to imitate ldap3 > 2.4.1
+LDAPDirectory_curly_objectGUID = [{"dn": 'cn=bob,ou=example,o=test',
+                 "attributes": {'cn': 'bob',
+                                "sn": "Marley",
+                                "givenName": "Robert",
+                                "email": "bob@example.com",
+                                "mobile": "123456",
+                                "homeDirectory": "/home/bob",
+                                'userPassword': 'bobpwééé',
+                                "accountExpires": 9223372036854775807,
+                                "objectGUID": "{" + objectGUIDs[0] + "}",
+                                'oid': "3"}},
+                {"dn": 'cn=manager,ou=example,o=test',
+                 "attributes": {'cn': 'manager',
+                                "givenName": "Corny",
+                                "sn": "keule",
+                                "email": "ck@o",
+                                "mobile": "123354",
+                                'userPassword': 'ldaptest',
+                                "accountExpires": 9223372036854775807,
+                                "objectGUID": "{" + objectGUIDs[1] + "}",
+                                'oid': "1"}}
+                       ]
 
 class SQLResolverTestCase(MyTestCase):
     """
@@ -559,8 +584,10 @@ def test_01_LDAP_DN(self):
         with mock.patch.object(ldap3mock.Connection.Extend.Standard, 'paged_search') as mock_search:
             def _search_with_ref(*args, **kwargs):
                 results = original_search(*args, **kwargs)
-                results.append({'type': 'searchResRef', 'foo': 'bar'})
-                return results
+                # paged_search returns an iterator
+                for result in results:
+                    yield result
+                yield {'type': 'searchResRef', 'foo': 'bar'}
 
             mock_search.side_effect = _search_with_ref
             ret = y.getUserList({"username": "bob"})
@@ -953,6 +980,42 @@ def test_09_test_objectGUID(self):
         res = y.checkPass(user_id, "test")
         self.assertTrue(res)
 
+    @ldap3mock.activate
+    def test_09b_test_curly_braced_objectGUID(self):
+        ldap3mock.setLDAPDirectory(LDAPDirectory_curly_objectGUID)
+        y = LDAPResolver()
+        y.loadConfig({'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",'
+                                  '"password" : "userPassword",'
+                                  '"email" : "mail",'
+                                  '"surname" : "sn",'
+                                  '"givenname" : "givenName" }',
+                      'UIDTYPE': 'objectGUID',
+                      'NOREFERRALS': True,
+                      'CACHE_TIMEOUT': 0
+        })
+        user_id = y.getUserId("bob")
+        res = y.checkPass(user_id, "bobpwééé")
+        self.assertTrue(res)
+
+        # Test changing the password
+        res = y.update_user(user_id, {"password": "test"})
+        self.assertTrue(res)
+
+        user_id = y.getUserId("bob")
+        self.assertEqual(user_id, objectGUIDs[0])
+        self.assertNotIn("{", user_id)
+        self.assertNotIn("}", user_id)
+        res = y.checkPass(user_id, "test")
+        self.assertTrue(res)
+
     def test_10_escape_loginname(self):
         r = LDAPResolver._escape_loginname("hans*")
         self.assertEqual(r, "hans\\2a")
@@ -1455,6 +1518,88 @@ def test_28_LDAP_multivalues(self):
         self.assertTrue("value1" in info.get("piAttr"))
         self.assertTrue("value2" in info.get("piAttr"))
 
+    @ldap3mock.activate
+    def test_29_sizelimit(self):
+        # This tests usernames are entered in the LDAPresolver as unicode.
+        ldap3mock.setLDAPDirectory(LDAPDirectory)
+        y = LDAPResolver()
+        y.loadConfig({'LDAPURI': 'ldap://localhost',
+                      'LDAPBASE': 'o=test',
+                      'BINDDN': 'cn=manager,ou=example,o=test',
+                      'BINDPW': 'ldaptest',
+                      'LOGINNAMEATTRIBUTE': 'cn, email',
+                      'LDAPSEARCHFILTER': '(cn=*)',
+                      'USERINFO': '{"phone" : "telephoneNumber", '
+                                  '"mobile" : "mobile"'
+                                  ', "email" : "email", '
+                                  '"surname" : "sn", '
+                                  '"givenname" : "givenName",'
+                                  '"piAttr": "someAttr"}',
+                      'UIDTYPE': 'DN',
+                      "SIZELIMIT": "3"
+                      })
+
+        result = y.getUserList({'username': '*'})
+        self.assertEqual(len(result), 3)
+
+        # We imitate ldap3 2.4.1 and raise an exception without having returned any entries
+        original_search = y.l.extend.standard.paged_search
+        with mock.patch.object(ldap3mock.Connection.Extend.Standard, 'paged_search') as mock_search:
+            def _search_with_exception(*args, **kwargs):
+                results = original_search(*args, **kwargs)
+                raise LDAPOperationResult(result=RESULT_SIZE_LIMIT_EXCEEDED)
+                # This ``yield`` is needed to turn this function into a generator.
+                # If we omit this, the exception above would be raised immediately when ``paged_search`` is called.
+                yield
+
+            mock_search.side_effect = _search_with_exception
+            ret = y.getUserList({"username": "*"})
+            self.assertTrue(mock_search.called)
+            # We get all three entries, due to the workaround in ``ignore_sizelimit_exception``!
+            self.assertEqual(len(ret), 3)
+
+
+        # We imitate a hypothetical later ldap3 version and raise an exception *after having returned all entries*!
+        # As ``getUserList`` stops consuming the generator after the size limit has been reached, we can only
+        # test this using testconnection.
+        with mock.patch.object(ldap3mock.Connection.Extend.Standard, 'paged_search', autospec=True) as mock_search:
+            # This is essentially a reimplementation of ``paged_search``
+            def _search_with_exception(self, **kwargs):
+                self.connection.search(search_base=kwargs.get("search_base"),
+                                       search_scope=kwargs.get("search_scope"),
+                                       search_filter=kwargs.get(
+                                           "search_filter"),
+                                       attributes=kwargs.get("attributes"),
+                                       paged_size=kwargs.get("page_size"),
+                                       size_limit=kwargs.get("size_limit"),
+                                       paged_cookie=None)
+                result = self.connection.response
+                assert kwargs['generator']
+                # Only return one result
+                yield result[0]
+                raise LDAPOperationResult(result=RESULT_SIZE_LIMIT_EXCEEDED)
+
+            mock_search.side_effect = _search_with_exception
+            ret = y.testconnection({'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': 'oid',
+                                    'CACHE_TIMEOUT': 0,
+                                    'SIZELIMIT': '1',
+                                    })
+            self.assertTrue(mock_search.called)
+            # We do not get any duplicate entries, due to the workaround in ``ignore_sizelimit_exception``!
+            self.assertTrue(ret[0])
+            self.assertEqual(ret[1], u'Your LDAP config seems to be OK, 1 user objects found.')
 
 class BaseResolverTestCase(MyTestCase):