Repository: privacyidea/privacyidea

Issue:
SQLResolver: Support userid as integer with userstore backend is postgesql
# What did you try to do?

Use the SQLIdResolver to verify admin realm user passwords for UI and API authentication.  Backend is PostgreSQL 9.5, single table w

# What outcome did you expect?

Authentication should have been successful ;-)

# What outcome did you experience?

It was not successful.  Digging into it, the offending query that PI was trying to run was

```
2019-03-19 19:46:43 UTC [30846-28] app_privacyidea@jhcache ERROR:  operator does not exist: integer ~~ integer at character 335
2019-03-19 19:46:43 UTC [30846-29] app_privacyidea@jhcache HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
2019-03-19 19:46:43 UTC [30846-30] app_privacyidea@jhcache STATEMENT:  SELECT v_privacyidea_users.account_id AS v_privacyidea_users_account_id, v_privacyidea_users.login AS v_privacyidea_users_login, v_privacyidea_users.password AS v_privacyidea_users_password, v_privacyidea_users.privacyidea_realm AS v_privacyidea_users_privacyidea_realm
	FROM v_privacyidea_users
	WHERE v_privacyidea_users.account_id LIKE 12756 AND v_privacyidea_users.privacyidea_realm = 'xandr_admins'
```

# Configuration

* privacyIDEA Version: 2.23.4-1xenial from the Ubuntu 16.04 upstream

* Installation method: Ubuntu main distro

* OS: Ubuntu 16.04

* Webserver: ngnix 1.10.3-0ubuntu0.16.04.3

* Tokendatabase: MySQL (installation default currently)

# Log file

relevant log entry
```
[2019-03-19 19:32:15,021][28418][139893765105536][INFO][privacyidea.lib.pooling:84] Creating a new engine and connection pool for key sql.442299badd9b6b76567c0559194b613c56745f8f
[2019-03-19 19:32:15,021][28418][139893765105536][INFO][privacyidea.lib.resolvers.SQLIdResolver:409] using the connect string postgres://app_privacyidea:xxxx@127.0.0.1:5432/jhcache
[2019-03-19 19:32:15,078][28418][139893765105536][INFO][privacyidea.lib.user:230] user u'app_mfa-ui' found in resolver u'xandr_admins'
[2019-03-19 19:32:15,079][28418][139893765105536][INFO][privacyidea.lib.user:231] userid resolved to 12756
[2019-03-19 19:32:15,085][28418][139893765105536][INFO][privacyidea.lib.user:346] User u'app_mfa-ui' from realm u'xandr_admins' tries to authenticate
[2019-03-19 19:32:15,088][28418][139893765105536][ERROR][privacyidea.lib.resolvers.SQLIdResolver:220] Could not get the userinformation: ProgrammingError('(psycopg2.ProgrammingError) operator does not exist: integer ~~ integer\nLINE 3: WHERE v_privacyidea_users.account_id LIKE 12756 AND v_privac...\n                                             ^\nHINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.\n',)
[2019-03-19 19:32:15,088][28418][139893765105536][ERROR][privacyidea.lib.user:366] Error checking password within module TypeError("object of type 'NoneType' has no len()",)
```

# More Info

Two realms, a user and admin realm, both in the same PostgreSQL table, differentiated by a column indicating the realm.
```
jhcache=# \d+ v_privacyidea_users
                            Table "public.v_privacyidea_users"
      Column       |       Type        | Modifiers | Storage  | Stats target | Description
-------------------+-------------------+-----------+----------+--------------+-------------
 account_id        | integer           | not null  | plain    |              |
 login             | character varying |           | extended |              |
 password          | character varying |           | extended |              |
 privacyidea_realm | character varying | not null  | extended |              |
Indexes:
    "v_privacyidea_users_pkey" PRIMARY KEY, btree (account_id, privacyidea_realm)
```

Problem lies in the fact that the getUserInfo assumes that the userid attribute is a string so it uses the .like filter, which makes postgres unhappy with the integerness of that account_id column.  Easy enough to fix, integer checks should be using the equality operator.


Discussion:
You can simply adapt your attribute mapping.
There is no reason to use "account_id" as ``id``. You could also use ``login``.
See #862.
I think we should close both tickets since the generic approach of the SQLResolver is not intended for such cases.
@fredreichbier @plettich:  what do you think

Resolving patch:
diff --git a/privacyidea/lib/resolvers/SQLIdResolver.py b/privacyidea/lib/resolvers/SQLIdResolver.py
--- a/privacyidea/lib/resolvers/SQLIdResolver.py
+++ b/privacyidea/lib/resolvers/SQLIdResolver.py
@@ -39,6 +39,7 @@
 
 from sqlalchemy import and_
 from sqlalchemy import create_engine
+from sqlalchemy import Integer
 from sqlalchemy.orm import sessionmaker, scoped_session
 
 import traceback
@@ -336,8 +337,7 @@ def getUserInfo(self, userId):
 
         try:
             conditions = []
-            column = self.map.get("userid")
-            conditions.append(getattr(self.TABLE, column).like(userId))
+            conditions.append(self._get_userid_filter(userId))
             conditions = self._append_where_filter(conditions, self.TABLE,
                                                    self.where)
             filter_condition = and_(*conditions)
@@ -351,6 +351,13 @@ def getUserInfo(self, userId):
             log.error("Could not get the userinformation: {0!r}".format(exx))
 
         return userinfo
+    
+    def _get_userid_filter(self, userId):
+        column = getattr(self.TABLE, self.map.get("userid"))
+        if isinstance(column.type, Integer):
+            return column == userId
+        else:
+            return column.like(userId)
 
     def getUsername(self, userId):
         """
@@ -720,8 +727,7 @@ def delete_user(self, uid):
         res = True
         try:
             conditions = []
-            column = self.map.get("userid")
-            conditions.append(getattr(self.TABLE, column).like(uid))
+            conditions.append(self._get_userid_filter(uid))
             conditions = self._append_where_filter(conditions, self.TABLE,
                                                    self.where)
             filter_condition = and_(*conditions)


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
@@ -514,6 +514,14 @@ def test_08_resolver_id(self):
 
         # rid1 != rid4, because the pool size has changed
         self.assertNotEqual(rid1, rid4)
+    
+    def test_08_noninteger_userid(self):
+        y = SQLResolver()
+        y.loadConfig(self.parameters)
+        y.map["userid"] = "username"
+        user = "cornelius"
+        user_info = y.getUserInfo(user)
+        self.assertEqual(user_info.get("id"), "cornelius")      
 
     def test_99_testconnection_fail(self):
         y = SQLResolver()