Repository: privacyidea/privacyidea

Issue:
Allow using objectGUID as login name
Especially when using the ownCloud app, it would be useful to be able to specify ``objectGUID`` as the login name attribute. Right now, this does not work because
* for ldap3 2.5, all login names are considered to be ``{``
* for ldap3 2.1.1, we get the first character of the objectGUID as the login name



Discussion:
What does  are considered to be ``{`` mean?

I also think we need to enhance the search string here, to be able to also find objectGUIDs:
https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/resolvers/LDAPIdResolver.py#L532

Interesting thing, which I totally forgot: The ldap resolver supports several login attributes comma separated. I.e. you can setup

    sAMAccountName, email

And the user can either login with his sAMAccountName or with his email address. Since logging in with once objectGUID is really wired, we could also only allow this in such a list.

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
@@ -489,7 +489,11 @@ def _ldap_attributes_to_user_object(self, attributes):
             for map_k, map_v in self.userinfo.items():
                 if ldap_k == map_v:
                     if ldap_k == "objectGUID":
-                        ret[map_k] = ldap_v[0]
+                        # An objectGUID should be no list, since it is unique
+                        if isinstance(ldap_v, basestring):
+                            ret[map_k] = ldap_v.strip("{").strip("}")
+                        else:
+                            raise Exception("The LDAP returns an objectGUID, that is no string: {0!s}".format(type(ldap_v)))
                     elif type(ldap_v) == list and map_k not in self.multivalueattributes:
                         # lists that are not in self.multivalueattributes return first value
                         # as a string. Multi-value-attributes are returned as a list
@@ -529,12 +533,28 @@ def getUserId(self, LoginName):
         if len(self.loginname_attribute) > 1:
             loginname_filter = u""
             for l_attribute in self.loginname_attribute:
-                loginname_filter += u"({!s}={!s})".format(l_attribute.strip(),
-                                                          login_name)
+                # Special case if we have a guid
+                try:
+                    if l_attribute.lower() == "objectguid":
+                        search_login_name = trim_objectGUID(login_name)
+                    else:
+                        search_login_name = login_name
+                    loginname_filter += u"({!s}={!s})".format(l_attribute.strip(),
+                                                              search_login_name)
+                except ValueError:
+                    # This happens if we have a self.loginname_attribute like ["sAMAccountName","objectGUID"],
+                    # the user logs in with his sAMAccountName, which can
+                    # not be transformed to a UUID
+                    log.debug(u"Can not transform {0!s} to a objectGUID.".format(login_name))
+
             loginname_filter = u"|" + loginname_filter
         else:
+            if self.loginname_attribute[0].lower() == "objectguid":
+                search_login_name = trim_objectGUID(login_name)
+            else:
+                search_login_name = login_name
             loginname_filter = u"{!s}={!s}".format(self.loginname_attribute[0],
-                                                   login_name)
+                                                   search_login_name)
 
         log.debug("login name filter: {!r}".format(loginname_filter))
         filter = u"(&{0!s}({1!s}))".format(self.searchfilter, loginname_filter)
@@ -674,7 +694,7 @@ def loadConfig(self, config):
         self.timeout = float(config.get("TIMEOUT", 5))
         self.cache_timeout = int(config.get("CACHE_TIMEOUT", 120))
         self.sizelimit = int(config.get("SIZELIMIT", 500))
-        self.loginname_attribute = config.get("LOGINNAMEATTRIBUTE","").split(",")
+        self.loginname_attribute = [la.strip() for la in config.get("LOGINNAMEATTRIBUTE","").split(",")]
         self.searchfilter = config.get("LDAPSEARCHFILTER")
         userinfo = config.get("USERINFO", "{}")
         self.userinfo = yaml.safe_load(userinfo)


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
@@ -1646,6 +1646,75 @@ def _search_with_exception(self, **kwargs):
                             ret[1])
 
 
+    @ldap3mock.activate
+    def test_30_login_with_objectGUID(self):
+        ldap3mock.setLDAPDirectory(LDAPDirectory)
+        classes = 'top, inetOrgPerson'
+        y = LDAPResolver()
+        y.loadConfig({'LDAPURI': 'ldap://localhost',
+                      'LDAPBASE': 'o=test',
+                      'BINDDN': 'cn=manager,ou=example,o=test',
+                      'BINDPW': 'ldaptest',
+                      'LOGINNAMEATTRIBUTE': 'cn,objectGUID, email',
+                      'LDAPSEARCHFILTER': '(cn=*)',
+                      'USERINFO': '{ "username": "cn",'
+                                  '"phone" : "telephoneNumber", '
+                                  '"mobile" : "mobile",'
+                                  '"email" : "email",'
+                                  '"userid" : "objectGUID",'
+                                  '"password" : "userPassword",'
+                                  '"surname" : "sn", '
+                                  '"givenname" : "givenName", '
+                                  '"accountExpires": "accountExpires" }',
+                      'UIDTYPE': 'objectGUID',
+                      'DN_TEMPLATE': 'cn=<username>,ou=example,o=test',
+                      'OBJECT_CLASSES': classes,
+                      'NOREFERRALS': True,
+                      'CACHE_TIMEOUT': 0
+                      })
+
+        # Alice, the email address and objectGUID of alice return the same uid.
+        uid = y.getUserId("alice")
+        self.assertEqual(uid, objectGUIDs[0])
+        uid = y.getUserId(objectGUIDs[0])
+        self.assertEqual(uid, objectGUIDs[0])
+        uid = y.getUserId("alice@test.com")
+        self.assertEqual(uid, objectGUIDs[0])
+
+    @ldap3mock.activate
+    def test_31_login_with_objectGUID_only(self):
+        ldap3mock.setLDAPDirectory(LDAPDirectory)
+        classes = 'top, inetOrgPerson'
+        y = LDAPResolver()
+        y.loadConfig({'LDAPURI': 'ldap://localhost',
+                      'LDAPBASE': 'o=test',
+                      'BINDDN': 'cn=manager,ou=example,o=test',
+                      'BINDPW': 'ldaptest',
+                      'LOGINNAMEATTRIBUTE': 'objectGUID',
+                      'LDAPSEARCHFILTER': '(cn=*)',
+                      'USERINFO': '{ "username": "cn",'
+                                  '"phone" : "telephoneNumber", '
+                                  '"mobile" : "mobile",'
+                                  '"email" : "email",'
+                                  '"userid" : "objectGUID",'
+                                  '"password" : "userPassword",'
+                                  '"surname" : "sn", '
+                                  '"givenname" : "givenName", '
+                                  '"accountExpires": "accountExpires" }',
+                      'UIDTYPE': 'objectGUID',
+                      'DN_TEMPLATE': 'cn=<username>,ou=example,o=test',
+                      'OBJECT_CLASSES': classes,
+                      'NOREFERRALS': True,
+                      'CACHE_TIMEOUT': 0
+                      })
+
+        # We can now authenticate using the objectGUID
+        uid = y.getUserId(objectGUIDs[0])
+        self.assertEqual(uid, objectGUIDs[0])
+
+        info = y.getUserInfo(uid)
+        self.assertEqual(info['username'], objectGUIDs[0])
+
 class BaseResolverTestCase(MyTestCase):
 
     def test_00_basefunctions(self):