Repository: privacyidea/privacyidea

Issue:
LDAP Resolver: Values are not fetched properly depending on ldap3 version
(detached from #887)

Both issues seem to have similar reasons, see cannatag/ldap3#482. i.e. they occur if we use ``entryUUID`` as the UID type.
Disabling fetching of schema information seems to mitigate the issue.

User info is incomplete with ldap3 2.4
==============================

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"
}
```

Crash with ldap3 2.4.1
==================
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;"
```




Discussion:
Turns out we have a similar problem on ldap3 2.4.1 for AD + objectGUID. In this case, disabling fetching schema info does not mitigate the problem, but produces a lot of new errors.

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
@@ -319,6 +319,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):


Test patch:
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
@@ -102,7 +102,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):
     """
@@ -953,6 +976,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")