Repository: privacyidea/privacyidea

Issue:
Realm name is case sensitive when login to the WebUI
The login as an admin on the webGui is key sensitive.

Example:
 
xxxx@helpdesk => Login is functioning. User has the admin rights

xxxx@Helpdesk => Login is possible, but the user doesn't have the admin rights.

Privacyidea 3.7.3


* [ ] Fix login at webui (/auth)
* ~~[ ] We need check if the same problem exists in /validate/check or in other endpoints to identify the user~~
  This only affects the admin rights, the authentication in the realm works correctly. Realm are always lower case.


Discussion:
@sepp67 We use projects for weekly development. Do not add issues to projects. If you think so, add an issue to a milestone (which is the release). But release 3.7 is closed. ;-) 
So if you really would think (what I do not think so), that this needs to be fixed in a 3.7patch, move it there. But I will not add this to 3.7, since the impact is too big.

Also - I think this is a duplicate, we already hat this issue during the 3.8 development. Need to look for that.

Ah, it is the case sensitive realm!
When we create a user object, we automatically convert a given realm to lower case:
https://github.com/privacyidea/privacyidea/blob/c4d57b59fe9b053fce8cc41d0c3e365dc1c473b3/privacyidea/lib/user.py#L93
So the user can be found regardless of the given realms case.
But when we check for the rights of the admin, we pass the realm as is (not from the user object). Therefor the admin doesn't have any rights:
https://github.com/privacyidea/privacyidea/blob/4bd26b740d709d568980cbc6961fa373df6a2b56/privacyidea/api/auth.py#L352


Resolving patch:
diff --git a/privacyidea/api/auth.py b/privacyidea/api/auth.py
--- a/privacyidea/api/auth.py
+++ b/privacyidea/api/auth.py
@@ -114,7 +114,7 @@ def before_request():
         # On endpoints like /auth/rights, this is not available
         loginname, realm = split_user(username)
         # overwrite the split realm if we have a realm parameter. Default back to default_realm
-        realm = getParam(request.all_data, "realm", default=realm) or realm or get_default_realm()
+        realm = getParam(request.all_data, "realm", default=realm) or get_default_realm()
         # Prefill the request.User. This is used by some pre-event handlers
         try:
             request.User = User(loginname, realm)
@@ -226,14 +226,15 @@ def get_auth_token():
         raise AuthError(_("Authentication failure. Missing Username"),
                         id=ERROR.AUTHENTICATE_MISSING_USERNAME)
 
-    loginname, realm = split_user(username)
-
-    # overwrite the split realm if we have a realm parameter
-    if realm_param:
-        realm = realm_param
-
-    # and finally check if there is a realm
-    realm = realm or get_default_realm()
+    user_obj = request.User
+    if not user_obj:
+        # The user could not be resolved, but it could still be a local administrator
+        loginname, realm = split_user(username)
+        realm = (realm_param or realm or get_default_realm()).lower()
+        user_obj = User()
+    else:
+        realm = user_obj.realm
+        loginname = user_obj.login
 
     # Failsafe to have the user attempt in the log, whatever happens
     # This can be overwritten later
@@ -241,8 +242,8 @@ def get_auth_token():
                         "realm": realm})
 
     secret = current_app.secret_key
-    superuser_realms = current_app.config.get("SUPERUSER_REALM", [])
-    # This is the default role for the logged in user.
+    superuser_realms = [x.lower() for x in current_app.config.get("SUPERUSER_REALM", [])]
+    # This is the default role for the logged-in user.
     # The role privileges may be risen to "admin"
     role = ROLE.USER
     # The way the user authenticated. This could be
@@ -254,8 +255,6 @@ def get_auth_token():
     admin_auth = False
     user_auth = False
 
-    user_obj = User()
-
     # Check if the remote user is allowed
     if (request.remote_user == username) and is_remote_user_allowed(request) != REMOTE_USER.DISABLE:
         # Authenticated by the Web Server
@@ -271,9 +270,9 @@ def get_auth_token():
                                 "user": "",
                                 "administrator": username,
                                 "info": "internal admin"})
+            user_obj = User()
         else:
             # check, if the user exists
-            user_obj = User(loginname, realm)
             g.audit_object.log({"user": user_obj.login,
                                 "realm": user_obj.realm,
                                 "info": log_used_user(user_obj)})
@@ -289,8 +288,10 @@ def get_auth_token():
         log.info("Local admin '{0!s}' successfully logged in.".format(username))
         # This admin is not in the default realm!
         realm = ""
+        user_obj = User()
         g.audit_object.log({"success": True,
                             "user": "",
+                            "realm": "",
                             "administrator": username,
                             "info": "internal admin"})
 
@@ -302,7 +303,6 @@ def get_auth_token():
         for key, value in request.all_data.items():
             if value and key not in ["g", "clientip"]:
                 options[key] = value
-        user_obj = User(loginname, realm)
         user_auth, role, details = check_webui_user(user_obj,
                                                     password,
                                                     options=options,
@@ -311,13 +311,13 @@ def get_auth_token():
         details = details or {}
         serials = ",".join([challenge_info["serial"] for challenge_info in details["multi_challenge"]]) \
             if 'multi_challenge' in details else details.get('serial')
-        if db_admin_exist(loginname) and user_auth and realm == get_default_realm():
+        if db_admin_exist(user_obj.login) and user_auth and realm == get_default_realm():
             # If there is a local admin with the same login name as the user
             # in the default realm, we inform about this in the log file.
             # This condition can only be checked if the user was authenticated as it
             # is the only way to verify if such a user exists.
             log.warning("A user '{0!s}' exists as local admin and as user in "
-                        "your default realm!".format(loginname))
+                        "your default realm!".format(user_obj.login))
         if role == ROLE.ADMIN:
             g.audit_object.log({"user": "",
                                 "administrator": user_obj.login,


Test patch:
diff --git a/tests/test_api_auth.py b/tests/test_api_auth.py
--- a/tests/test_api_auth.py
+++ b/tests/test_api_auth.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 """ Test for the '/auth' API-endpoint """
-from .base import MyApiTestCase
+from .base import MyApiTestCase, OverrideConfigTestCase
 import mock
 import six
 from privacyidea.lib.config import set_privacyidea_config, SYSCONF
@@ -14,6 +14,7 @@
 from privacyidea.lib.token import get_tokens, remove_token
 from privacyidea.lib.user import User
 from privacyidea.lib.utils import to_unicode
+from privacyidea.config import TestingConfig
 from . import ldap3mock
 
 
@@ -555,6 +556,89 @@ def test_04_remote_user_auth(self):
         delete_policy(name='remote')
         set_privacyidea_config(SYSCONF.SPLITATSIGN, True)
 
+    @ldap3mock.activate
+    def test_05_local_admin_with_failing_resolver(self):
+        ldap3mock.setLDAPDirectory([])
+        # define, that we want to get an exception
+        ldap3mock.set_exception()
+        # Create an LDAP Realm as default realm
+        params = {'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': 'DN',
+                  "resolver": "ldap1",
+                  "type": "ldapresolver"}
+        save_resolver(params)
+        set_realm("ldap1", ["ldap1"])
+        set_default_realm("ldap1")
+
+        # Try to log in as internal admin with a failing LDAP resolver
+        with mock.patch("logging.Logger.warning") as mock_log:
+            with self.app.test_request_context('/auth',
+                                               method='POST',
+                                               data={"username": "testadmin",
+                                                     "password": "testpw"}):
+                res = self.app.full_dispatch_request()
+                self.assertEqual(200, res.status_code, res)
+                result = res.json.get("result")
+                self.assertTrue(result.get("status"), result)
+                self.assertIn('token', result.get("value"), result)
+                # role should be 'admin'
+                self.assertEqual('admin', result['value']['role'], result)
+                mock_log.assert_called_once_with("Problem resolving user testadmin in realm ldap1: LDAP request failed.")
+
+        delete_realm("ldap1")
+        delete_resolver("ldap1")
+        ldap3mock.set_exception(False)
+
+
+class AdminFromUserstore(OverrideConfigTestCase):
+    class Config(TestingConfig):
+        SUPERUSER_REALM = ["realm1"]
+
+    def test_01_admin_from_userstore(self):
+        self.setUp_user_realms()
+        # login as an admin user from userstore
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "cornelius@realm1",
+                                                 "password": "test"}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 200, res)
+            result = res.json.get("result")
+            self.assertTrue(result.get("status"), result)
+            self.assertIn('token', result.get('value'), result)
+            self.assertEqual(result.get("value").get('role'), 'admin', result)
+            # if no admin policy is set, the user should have all admin rights
+            self.assertIn(ACTION.DELETE, result.get('value').get('rights'), result)
+
+        # Now test with a helpdesk policy for the admin realm
+        set_policy(name='helpdesk', scope=SCOPE.ADMIN, adminrealm=self.realm1,
+                   action=ACTION.DISABLE)
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "cornelius@Realm1",
+                                                 "password": "test"}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 200, res)
+            result = res.json.get("result")
+            self.assertTrue(result.get("status"), result)
+            self.assertIn('token', result.get('value'), result)
+            self.assertEqual(result.get("value").get('role'), 'admin', result)
+            # check that the appropriate rights are set/unset
+            self.assertNotIn(ACTION.DELETE, result.get('value').get('rights'), result)
+            self.assertIn(ACTION.DISABLE, result.get('value').get('rights'), result)
+        delete_policy(name='helpdesk')
+
 
 class DuplicateUserApiTestCase(MyApiTestCase):
 
@@ -622,7 +706,7 @@ def test_01_admin_and_user_same_name(self):
             mock_log.assert_called_with("user uid 1004 failed to authenticate")
 
 
-class AAPreEventHandlerTest(MyApiTestCase):
+class PreEventHandlerTest(MyApiTestCase):
 
     def test_01_setup_eventhandlers(self):
         # This test create an HOTP token with C/R with a pre-event handler
@@ -708,47 +792,3 @@ def test_01_setup_eventhandlers(self):
         delete_policy("crhotp")
         delete_event(eid)
         remove_token(hotptoken.token.serial)
-
-    @ldap3mock.activate
-    def test_02_failing_resolver(self):
-        ldap3mock.setLDAPDirectory([])
-        # define, that we want to get an exception
-        ldap3mock.set_exception()
-        # Create an LDAP Realm as default realm
-        params = {'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': 'DN',
-                  "resolver": "ldap1",
-                  "type": "ldapresolver"}
-        save_resolver(params)
-        set_realm("ldap1", ["ldap1"])
-        set_default_realm("ldap1")
-
-        # Try to login as internal admin with a failing LDAP resolver
-        with mock.patch("logging.Logger.warning") as mock_log:
-            with self.app.test_request_context('/auth',
-                                               method='POST',
-                                               data={"username": "testadmin",
-                                                     "password": "testpw"}):
-                res = self.app.full_dispatch_request()
-                self.assertEqual(200, res.status_code, res)
-                result = res.json.get("result")
-                self.assertTrue(result.get("status"), result)
-                self.assertIn('token', result.get("value"), result)
-                # role should be 'admin'
-                self.assertEqual('admin', result['value']['role'], result)
-                mock_log.assert_called_once_with("Problem resolving user testadmin in realm ldap1: LDAP request failed.")
-
-        delete_realm("ldap1")
-        delete_resolver("ldap1")
-        ldap3mock.set_exception(False)