Repository: privacyidea/privacyidea

Issue:
Make remote_user policy aware of split@Sign parameter
The [`is_remote_user_allowed()`](https://github.com/privacyidea/privacyidea/blob/eb0000f2c54b37c75b715e536eb23749ac487543/privacyidea/api/lib/prepolicy.py#L1153) policy does not respect the [`split@Sign`](https://privacyidea.readthedocs.io/en/latest/configuration/system_config.html#splitatsign)-configuration. See also #1808 


Discussion:
Check this in 3.4

Resolving patch:
diff --git a/privacyidea/api/auth.py b/privacyidea/api/auth.py
--- a/privacyidea/api/auth.py
+++ b/privacyidea/api/auth.py
@@ -207,12 +207,9 @@ def get_auth_token():
         raise AuthError(_("Authentication failure. Missing Username"),
                         id=ERROR.AUTHENTICATE_MISSING_USERNAME)
 
-    loginname = username
-    split_at_sign = get_from_config(SYSCONF.SPLITATSIGN, return_bool=True)
-    if split_at_sign:
-        (loginname, realm) = split_user(username)
+    loginname, realm = split_user(username)
 
-    # overwrite the splitted realm if we have a realm parameter
+    # overwrite the split realm if we have a realm parameter
     if realm_param:
         realm = realm_param
 
diff --git a/privacyidea/api/lib/prepolicy.py b/privacyidea/api/lib/prepolicy.py
--- a/privacyidea/api/lib/prepolicy.py
+++ b/privacyidea/api/lib/prepolicy.py
@@ -66,41 +66,43 @@
 """
 
 import logging
-import string
 
 from OpenSSL import crypto
 
 from privacyidea.lib.error import PolicyError, RegistrationError, TokenAdminError, ResourceNotFoundError
 from flask import g, current_app
-from privacyidea.lib.policy import SCOPE, ACTION, PolicyClass
+from privacyidea.lib.policy import SCOPE, ACTION, REMOTE_USER
 from privacyidea.lib.policy import Match
 from privacyidea.lib.user import (get_user_from_param, get_default_realm,
                                   split_user, User)
 from privacyidea.lib.token import (get_tokens, get_realms_of_token, get_token_type, get_token_owner)
-from privacyidea.lib.utils import (get_client_ip,
-                                   parse_timedelta, is_true, generate_charlists_from_pin_policy,
+from privacyidea.lib.utils import (parse_timedelta, is_true, generate_charlists_from_pin_policy,
                                    check_pin_policy, get_module_class,
                                    determine_logged_in_userparams)
 from privacyidea.lib.crypto import generate_password
 from privacyidea.lib.auth import ROLE
 from privacyidea.api.lib.utils import getParam, attestation_certificate_allowed, is_fqdn
 from privacyidea.lib.clientapplication import save_clientapplication
-from privacyidea.lib.config import (get_token_class, get_from_config, SYSCONF)
+from privacyidea.lib.config import (get_token_class)
 import functools
 import jwt
 import re
 import importlib
 
 # Token specific imports!
-from privacyidea.lib.tokens.webauthn import (WebAuthnRegistrationResponse, AUTHENTICATOR_ATTACHMENT_TYPES,
-                                             USER_VERIFICATION_LEVELS, ATTESTATION_LEVELS, ATTESTATION_FORMS)
-from privacyidea.lib.tokens.webauthntoken import (WEBAUTHNACTION, DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE,
+from privacyidea.lib.tokens.webauthn import (WebAuthnRegistrationResponse,
+                                             AUTHENTICATOR_ATTACHMENT_TYPES,
+                                             USER_VERIFICATION_LEVELS, ATTESTATION_LEVELS,
+                                             ATTESTATION_FORMS)
+from privacyidea.lib.tokens.webauthntoken import (WEBAUTHNACTION,
+                                                  DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE,
                                                   PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE_OPTIONS,
                                                   DEFAULT_TIMEOUT, DEFAULT_ALLOWED_TRANSPORTS,
                                                   DEFAULT_USER_VERIFICATION_REQUIREMENT,
                                                   DEFAULT_AUTHENTICATOR_ATTESTATION_LEVEL,
-                                                  DEFAULT_AUTHENTICATOR_ATTESTATION_FORM, WebAuthnTokenClass,
-                                                  DEFAULT_CHALLENGE_TEXT_AUTH, DEFAULT_CHALLENGE_TEXT_ENROLL,
+                                                  DEFAULT_AUTHENTICATOR_ATTESTATION_FORM,
+                                                  WebAuthnTokenClass, DEFAULT_CHALLENGE_TEXT_AUTH,
+                                                  DEFAULT_CHALLENGE_TEXT_ENROLL,
                                                   is_webauthn_assertion_response)
 from privacyidea.lib.tokens.u2ftoken import (U2FACTION, parse_registration_data)
 from privacyidea.lib.tokens.u2f import x509name_to_string
@@ -112,6 +114,7 @@
 optional = True
 required = False
 
+
 class prepolicy(object):
     """
     This is the decorator wrapper to call a specific function before an API
@@ -1243,7 +1246,10 @@ def is_remote_user_allowed(req):
                                      action=ACTION.REMOTE_USER,
                                      user=loginname,
                                      realm=realm).action_values(unique=False)
-        res = bool(ruser_active)
+        # there should be only one action value here
+        if ruser_active:
+            if list(ruser_active)[0] == REMOTE_USER.ACTIVE:
+                res = True
 
     return res
 
diff --git a/privacyidea/lib/user.py b/privacyidea/lib/user.py
--- a/privacyidea/lib/user.py
+++ b/privacyidea/lib/user.py
@@ -56,7 +56,7 @@
 from .resolver import (get_resolver_object,
                        get_resolver_type)
 
-from .realm import (get_realms,
+from .realm import (get_realms, realm_is_defined,
                     get_default_realm,
                     get_realm)
 from .config import get_from_config, SYSCONF
@@ -524,7 +524,10 @@ def split_user(username):
 
     If for a user@domain the "domain" does not exist as realm, the name is
     not split, since it might be the user@domain in the default realm
-    
+
+    If the Split@Sign configuration is disabled, the username won't be split
+    and the username and an empty realm will be returned.
+
     We can also split realm\\user to (user, realm)
     
     :param username: the username to split
@@ -532,19 +535,20 @@ def split_user(username):
     :return: username and realm
     :rtype: tuple
     """
-    from privacyidea.lib.realm import realm_is_defined
     user = username.strip()
     realm = ""
 
-    l = user.split('@')
-    if len(l) >= 2:
-        if realm_is_defined(l[-1]):
-            # split the last only if the last part is really a realm
-            (user, realm) = user.rsplit('@', 1)
-    else:
-        l = user.split('\\')
+    split_at_sign = get_from_config(SYSCONF.SPLITATSIGN, return_bool=True)
+    if split_at_sign:
+        l = user.split('@')
         if len(l) >= 2:
-            (realm, user) = user.rsplit('\\', 1)
+            if realm_is_defined(l[-1]):
+                # split the last only if the last part is really a realm
+                (user, realm) = user.rsplit('@', 1)
+        else:
+            l = user.split('\\')
+            if len(l) >= 2:
+                (realm, user) = user.rsplit('\\', 1)
 
     return user, realm
 
@@ -569,9 +573,7 @@ def get_user_from_param(param, optionalOrRequired=optional):
     if username is None:
         username = ""
     else:
-        splitAtSign = get_from_config(SYSCONF.SPLITATSIGN, return_bool=True)
-        if splitAtSign:
-            (username, realm) = split_user(username)
+        username, realm = split_user(username)
 
     if "realm" in param:
         realm = param["realm"]


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,11 +1,11 @@
 # -*- coding: utf-8 -*-
 """ Test for the '/auth' API-endpoint """
-
 from .base import MyApiTestCase
 import mock
 import six
-from privacyidea.lib.config import (set_privacyidea_config,
-                                    SYSCONF)
+from privacyidea.lib.config import set_privacyidea_config, SYSCONF
+from privacyidea.lib.policy import (set_policy, SCOPE, ACTION, REMOTE_USER,
+                                    delete_policy)
 from privacyidea.lib.auth import create_db_admin
 
 
@@ -422,3 +422,111 @@ def test_03_admin_auth(self):
 
         # reset 'splitAtSign' to default value
         set_privacyidea_config(SYSCONF.SPLITATSIGN, True)
+
+    def test_04_remote_user_auth(self):
+        self.setUp_user_realms()
+        # first check that without a remote_user policy the login fails
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "cornelius"},
+                                           environ_base={"REMOTE_USER": "cornelius"}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 401, res)
+            result = res.json.get("result")
+            self.assertFalse(result.get("status"), result)
+            self.assertEqual(result.get("error").get('code'), 4031, result)
+            self.assertEqual(result.get("error").get('message'),
+                             'Authentication failure. Wrong credentials', result)
+        aentry = self.find_most_recent_audit_entry(action='POST /auth')
+        self.assertEqual(aentry['action'], 'POST /auth', aentry)
+        self.assertEqual(aentry['success'], 0, aentry)
+
+        # now check that with a disabled remote_user policy the login fails
+        set_policy(name="remote", scope=SCOPE.WEBUI,
+                   action="{0!s}={1!s}".format(ACTION.REMOTE_USER, REMOTE_USER.DISABLE))
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "cornelius"},
+                                           environ_base={"REMOTE_USER": "cornelius"}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 401, res)
+            result = res.json.get("result")
+            self.assertFalse(result.get("status"), result)
+            self.assertEqual(result.get("error").get('code'), 4031, result)
+            self.assertEqual(result.get("error").get('message'),
+                             'Authentication failure. Wrong credentials', result)
+        aentry = self.find_most_recent_audit_entry(action='POST /auth')
+        self.assertEqual(aentry['action'], 'POST /auth', aentry)
+        self.assertEqual(aentry['success'], 0, aentry)
+        self.assertEqual(aentry['policies'], 'remote', aentry)
+
+        # And now check that with an enabled remote_user policy the login succeeds
+        set_policy(name="remote", scope=SCOPE.WEBUI,
+                   action="{0!s}={1!s}".format(ACTION.REMOTE_USER, REMOTE_USER.ACTIVE))
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "cornelius"},
+                                           environ_base={"REMOTE_USER": "cornelius"}):
+            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.assertEqual(result.get("value").get('role'), 'user', result)
+        aentry = self.find_most_recent_audit_entry(action='POST /auth')
+        self.assertEqual(aentry['action'], 'POST /auth', aentry)
+        self.assertEqual(aentry['success'], 1, aentry)
+        self.assertEqual(aentry['policies'], 'remote', aentry)
+
+        # check that a remote user with "@" works as well
+        set_policy(name="remote", scope=SCOPE.WEBUI, realm=self.realm1,
+                   action="{0!s}={1!s}".format(ACTION.REMOTE_USER, REMOTE_USER.ACTIVE))
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "cornelius@realm1"},
+                                           environ_base={"REMOTE_USER": "cornelius@realm1"}):
+            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.assertEqual(result.get("value").get('role'), 'user', result)
+        aentry = self.find_most_recent_audit_entry(action='POST /auth')
+        self.assertEqual(aentry['action'], 'POST /auth', aentry)
+        self.assertEqual(aentry['success'], 1, aentry)
+        self.assertEqual(aentry['policies'], 'remote', aentry)
+
+        # bind the remote user policy to an unknown realm
+        set_policy(name="remote", scope=SCOPE.WEBUI, realm='unknown',
+                   action="{0!s}={1!s}".format(ACTION.REMOTE_USER, REMOTE_USER.ACTIVE))
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "cornelius@realm1"},
+                                           environ_base={"REMOTE_USER": "cornelius@realm1"}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 401, res)
+            result = res.json.get("result")
+            self.assertFalse(result.get("status"), result)
+        aentry = self.find_most_recent_audit_entry(action='POST /auth')
+        self.assertEqual(aentry['action'], 'POST /auth', aentry)
+        self.assertEqual(aentry['success'], 0, aentry)
+        self.assertEqual(aentry['policies'], '', aentry)
+
+        # check split@sign is working correctly
+        set_policy(name="remote", scope=SCOPE.WEBUI, realm=self.realm1,
+                   action="{0!s}={1!s}".format(ACTION.REMOTE_USER, REMOTE_USER.ACTIVE))
+        set_privacyidea_config(SYSCONF.SPLITATSIGN, False)
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "user@test"},
+                                           environ_base={"REMOTE_USER": "user@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.assertEqual(result.get("value").get('role'), 'user', result)
+        aentry = self.find_most_recent_audit_entry(action='POST /auth')
+        self.assertEqual(aentry['action'], 'POST /auth', aentry)
+        self.assertEqual(aentry['success'], 1, aentry)
+        self.assertEqual(aentry['policies'], 'remote', aentry)
+
+        delete_policy(name='remote')
+        set_privacyidea_config(SYSCONF.SPLITATSIGN, True)
diff --git a/tests/test_api_lib_policy.py b/tests/test_api_lib_policy.py
--- a/tests/test_api_lib_policy.py
+++ b/tests/test_api_lib_policy.py
@@ -7,10 +7,11 @@
 from __future__ import print_function
 import json
 
-from privacyidea.lib.tokens.webauthn import (webauthn_b64_decode, AUTHENTICATOR_ATTACHMENT_TYPE, ATTESTATION_LEVEL,
-                                             ATTESTATION_FORM, USER_VERIFICATION_LEVEL)
-from privacyidea.lib.tokens.webauthntoken import (WEBAUTHNACTION, DEFAULT_ALLOWED_TRANSPORTS, WebAuthnTokenClass,
-                                                  DEFAULT_CHALLENGE_TEXT_AUTH,
+from privacyidea.lib.tokens.webauthn import (webauthn_b64_decode, AUTHENTICATOR_ATTACHMENT_TYPE,
+                                             ATTESTATION_LEVEL, ATTESTATION_FORM,
+                                             USER_VERIFICATION_LEVEL)
+from privacyidea.lib.tokens.webauthntoken import (WEBAUTHNACTION, DEFAULT_ALLOWED_TRANSPORTS,
+                                                  WebAuthnTokenClass, DEFAULT_CHALLENGE_TEXT_AUTH,
                                                   PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE_OPTIONS,
                                                   DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE,
                                                   DEFAULT_AUTHENTICATOR_ATTESTATION_LEVEL,
@@ -18,7 +19,8 @@
                                                   DEFAULT_CHALLENGE_TEXT_ENROLL, DEFAULT_TIMEOUT,
                                                   DEFAULT_USER_VERIFICATION_REQUIREMENT)
 from privacyidea.lib.utils import hexlify_and_unicode
-from .base import (MyApiTestCase, PWFILE)
+from privacyidea.lib.config import set_privacyidea_config, SYSCONF
+from .base import (MyApiTestCase)
 
 from privacyidea.lib.policy import (set_policy, delete_policy, enable_policy,
                                     PolicyClass, SCOPE, ACTION, REMOTE_USER,
@@ -39,10 +41,10 @@
                                            u2ftoken_verify_cert,
                                            tantoken_count, sms_identifiers,
                                            pushtoken_add_config, pushtoken_wait,
-                                           check_admin_tokenlist, pushtoken_disable_wait,
                                            indexedsecret_force_attribute,
-                                           check_admin_tokenlist, pushtoken_disable_wait, webauthntoken_auth,
-                                           webauthntoken_authz, webauthntoken_enroll, webauthntoken_request,
+                                           check_admin_tokenlist, pushtoken_disable_wait,
+                                           webauthntoken_auth, webauthntoken_authz,
+                                           webauthntoken_enroll, webauthntoken_request,
                                            webauthntoken_allowed, check_application_tokentype)
 from privacyidea.lib.realm import set_realm as create_realm
 from privacyidea.lib.realm import delete_realm
@@ -76,9 +78,11 @@
 from datetime import datetime, timedelta
 from dateutil.tz import tzlocal
 from privacyidea.lib.tokenclass import DATE_FORMAT
-from .test_lib_tokens_webauthn import (ALLOWED_TRANSPORTS, CRED_ID, ASSERTION_RESPONSE_TMPL, ASSERTION_CHALLENGE,
-                                       RP_ID, RP_NAME, ORIGIN, REGISTRATION_RESPONSE_TMPL)
-from privacyidea.lib.utils import create_img, generate_charlists_from_pin_policy, CHARLIST_CONTENTPOLICY, check_pin_policy
+from .test_lib_tokens_webauthn import (ALLOWED_TRANSPORTS, CRED_ID, ASSERTION_RESPONSE_TMPL,
+                                       ASSERTION_CHALLENGE, RP_ID, RP_NAME, ORIGIN,
+                                       REGISTRATION_RESPONSE_TMPL)
+from privacyidea.lib.utils import (create_img, generate_charlists_from_pin_policy,
+                                   CHARLIST_CONTENTPOLICY, check_pin_policy)
 
 
 HOSTSFILE = "tests/testdata/hosts"
@@ -1241,16 +1245,12 @@ def test_12_mangle(self):
         delete_policy("mangle2")
 
     def test_13_remote_user(self):
-        g.logged_in_user = {"username": "admin1",
-                            "realm": "",
-                            "role": "admin"}
         builder = EnvironBuilder(method='POST',
                                  data={'serial': "OATH123456"},
                                  headers={})
         env = builder.get_environ()
         # Set the remote address so that we can filter for it
         env["REMOTE_ADDR"] = "10.0.0.1"
-        g.client_ip = env["REMOTE_ADDR"]
         env["REMOTE_USER"] = "admin"
         req = Request(env)
 
@@ -1258,7 +1258,6 @@ def test_13_remote_user(self):
         set_policy(name="ruser",
                    scope=SCOPE.WEBUI,
                    action="{0!s}={1!s}".format(ACTION.REMOTE_USER, REMOTE_USER.ACTIVE))
-        g.policy_object = PolicyClass()
 
         r = is_remote_user_allowed(req)
         self.assertTrue(r)
@@ -1269,7 +1268,6 @@ def test_13_remote_user(self):
                    scope=SCOPE.WEBUI,
                    action="{0!s}={1!s}".format(ACTION.REMOTE_USER, REMOTE_USER.ACTIVE),
                    user="super")
-        g.policy_object = PolicyClass()
 
         r = is_remote_user_allowed(req)
         self.assertFalse(r)
@@ -1277,10 +1275,20 @@ def test_13_remote_user(self):
         # The remote_user "super" is allowed to login:
         env["REMOTE_USER"] = "super"
         req = Request(env)
-        g.policy_object = PolicyClass()
         r = is_remote_user_allowed(req)
         self.assertTrue(r)
 
+        # check that Splt@Sign works correctly
+        create_realm(self.realm1)
+        set_privacyidea_config(SYSCONF.SPLITATSIGN, True)
+        env["REMOTE_USER"] = "super@realm1"
+        req = Request(env)
+        self.assertTrue(is_remote_user_allowed(req))
+
+        set_privacyidea_config(SYSCONF.SPLITATSIGN, False)
+        self.assertFalse(is_remote_user_allowed(req))
+
+        set_privacyidea_config(SYSCONF.SPLITATSIGN, True)
         delete_policy("ruser")
 
     def test_14_required_email(self):
@@ -1910,7 +1918,7 @@ def test_25_admin_token_list(self):
             delete_policy(pol)
 
     def test_26_indexedsecret_force_set(self):
-
+        self.setUp_user_realms()
         # We send a fake push_wait, that is not in the policies
         builder = EnvironBuilder(method='POST',
                                  data={'user': "cornelius",
diff --git a/tests/testdata/passwords b/tests/testdata/passwords
--- a/tests/testdata/passwords
+++ b/tests/testdata/passwords
@@ -13,3 +13,4 @@ lockeduser:DgT1djMaFfpEs:1113:1113:::
 usernotoken::1114:1114:::
 multichal::1115:1115:::
 nönäscii:IXsH4ttQlw2xA:1116:1116:Nön,field2,field3,field4,field5::
+user@test::1117:1117:::