Repository: privacyidea/privacyidea

Issue:
user comparison based on uid
We are currently comparing user objects based on the loginname:

https://github.com/privacyidea/privacyidea/blob/0f085fd2ed59cd4567989c9bbe55ea01c29ced3a/privacyidea/lib/user.py#L150

I think this is due to the fact, that in some cases a user object could not be readily initialized and actually have no uid - or it was this way.

We are observing that we could run into case sensitivity problems, depending on the scenario, what a user enters as his username and what the resolver returns as a username. So I think we should compare user objects based on the uid, if it is available.

The following code should provide us backward compatibility (if there is a userobject without a uid) and also compare via uid, if it exists.

~~~~python
    def __eq__(self, other):
        """
        Compare two User Objects.

        :param other: The other User object, to which this very object is
        compared.
        :type other: User object
        :return: True or False
        :rtype: bool
        """
        if not isinstance(other, type(self)):
            log.info("comparing users of different classes.")
            return False
        if (self.resolver != other.resolver) or (self.realm != other.realm):
            log.info("Users are not in the same resolver and realm")
            return False
        if self.uid and other.uid:
            log.info("comparing based on uid")
            return self.uid == other.uid
        log.info("comparing based on login")
        return self.login == other.login
~~~~

We might add this to a 3.7.2.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/user.py b/privacyidea/lib/user.py
--- a/privacyidea/lib/user.py
+++ b/privacyidea/lib/user.py
@@ -147,8 +147,18 @@ def __eq__(self, other):
         :return: True or False
         :rtype: bool
         """
-        return isinstance(other, type(self)) and (self.login == other.login) and (
-                self.resolver == other.resolver) and (self.realm == other.realm)
+        if not isinstance(other, type(self)):
+            log.info("Comparing a non-user object: {0!s} != {1!s}.".format(self, type(other)))
+            return False
+        if (self.resolver != other.resolver) or (self.realm != other.realm):
+            log.info("Users are not in the same resolver and realm: "
+                     "{0!s} != {1!s}.".format(self, other))
+            return False
+        if self.uid and other.uid:
+            log.debug(u"Comparing based on uid: {0!s} vs {1!s}".format(self.uid, other.uid))
+            return self.uid == other.uid
+        log.debug(u"Comparing based on login: {0!s} vs {1!s}".format(self.login, other.login))
+        return self.login == other.login
 
     def __ne__(self, other):
         """


Test patch:
diff --git a/tests/test_lib_user.py b/tests/test_lib_user.py
--- a/tests/test_lib_user.py
+++ b/tests/test_lib_user.py
@@ -476,6 +476,46 @@ def test_18_user_with_several_phones(self):
         delete_realm("ldap")
         delete_resolver("ldapresolver")
 
+    @ldap3mock.activate
+    def test_19_compare_user_object(self):
+        ldap3mock.setLDAPDirectory(LDAPDirectory_small)
+        params = ({'LDAPURI': 'ldap://localhost',
+                   'LDAPBASE': 'o=test',
+                   'BINDDN': 'cn=manager,ou=example,o=test',
+                   'BINDPW': 'ldaptest',
+                   'LOGINNAMEATTRIBUTE': 'cn',
+                   'LDAPSEARCHFILTER': '(|(cn=*))',  # we use this weird search filter to get a unique resolver ID
+                   'USERINFO': '{ "username": "cn",'
+                               '"phone" : "telephoneNumber", '
+                               '"mobile" : "mobile"'
+                               ', "email" : "mail", '
+                               '"surname" : "sn", '
+                               '"givenname" : "givenName" }',
+                   'UIDTYPE': 'objectGUID',
+                   'NOREFERRALS': True,
+                   'CACHE_TIMEOUT': 0
+                   })
+        params["resolver"] = "ldapresolver"
+        params["type"] = "ldapresolver"
+        rid = save_resolver(params)
+        self.assertTrue(rid > 0)
+        (added, failed) = set_realm("ldap", ["ldapresolver"])
+        self.assertEqual(len(added), 1)
+        self.assertEqual(len(failed), 0)
+
+        # Comparing different objects fails
+        self.assertFalse(User("salesman", "ldap") == None)
+        # comparing different realm or resolver fails
+        self.assertFalse(User("cornelius", self.realm1) == User("salesman", "ldap"))
+        # comparing different users fails
+        self.assertFalse(User("manager", "ldap") == User("salesman", "ldap"))
+        # comparing case insensitive successful
+        self.assertTrue(User("salesman", "ldap") == User("salesman", "ldap"))
+        self.assertTrue(User(resolver='ldapresolver', realm="ldap",
+                             uid="039b36ef-e7c0-42f3-9bf9-ca6a6c0d4d54") == User("salesman", "ldap"))
+        delete_realm("ldap")
+        delete_resolver("ldapresolver")
+
     def test_50_user_attributes(self):
         user = User(login="root",
                     realm=self.realm1)