Repository: privacyidea/privacyidea

Issue:
Update `admin_or_user()` policy matcher
The `admin_or_user()` function needs an update to reflect changes from #1867 (Add adminuser and resolver)


Discussion:
Currently the Match.admin_or_user only matches for the *userrealm* in case the logged_in_user is an admin.
It should now match against the complete user, which is usually contained in request.User.realm.

So we could change the signature of the function to:

~~~~python
def admin_or_user(cls, g, action, user=None)
     user = user or request.user
~~~~
The policy match contructors ``admin_or_user(cls, g, action, realm)`` and ``admin(cls, g, action, realm)`` should both request a parameter ``user_obj`` instead of the realm parameter.

Resolving patch:
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
@@ -226,7 +226,7 @@ def realmadmin(request=None, action=None):
         params = request.all_data
         if not "realm" in params:
             # add the realm to params
-            po = Match.admin(g, action=action, realm=None).policies()
+            po = Match.admin(g, action=action).policies()
             # TODO: fix this: there could be a list of policies with a list
             # of realms!
             if po and po[0].get("realm"):
@@ -267,25 +267,24 @@ def check_otp_pin(request=None, action=None):
             tokentype = tokensobject_list[0].token.tokentype
     # the default tokentype is still HOTP
     tokentype = tokentype or "hotp"
-    if g.logged_in_user.get("role") == ROLE.ADMIN:
-        realm = params.get("realm", "")
-    else:
-        realm = g.logged_in_user.get("realm")
     # get the policies for minimum length, maximum length and PIN contents
     # first try to get a token specific policy - otherwise fall back to
     # default policy
     pol_minlen = Match.admin_or_user(g, action="{0!s}_{1!s}".format(tokentype, ACTION.OTPPINMINLEN),
-                                     realm=realm).action_values(unique=True)
+                                     user_obj=request.User).action_values(unique=True)
     if not pol_minlen:
-        pol_minlen = Match.admin_or_user(g, action=ACTION.OTPPINMINLEN, realm=realm).action_values(unique=True)
+        pol_minlen = Match.admin_or_user(g, action=ACTION.OTPPINMINLEN,
+                                         user_obj=request.User).action_values(unique=True)
     pol_maxlen = Match.admin_or_user(g, action="{0!s}_{1!s}".format(tokentype, ACTION.OTPPINMAXLEN),
-                                     realm=realm).action_values(unique=True)
+                                     user_obj=request.User).action_values(unique=True)
     if not pol_maxlen:
-        pol_maxlen = Match.admin_or_user(g, action=ACTION.OTPPINMAXLEN, realm=realm).action_values(unique=True)
+        pol_maxlen = Match.admin_or_user(g, action=ACTION.OTPPINMAXLEN,
+                                         user_obj=request.User).action_values(unique=True)
     pol_contents = Match.admin_or_user(g, action="{0!s}_{1!s}".format(tokentype, ACTION.OTPPINCONTENTS),
-                                       realm=realm).action_values(unique=True)
+                                       user_obj=request.User).action_values(unique=True)
     if not pol_contents:
-        pol_contents = Match.admin_or_user(g, action=ACTION.OTPPINCONTENTS, realm=realm).action_values(unique=True)
+        pol_contents = Match.admin_or_user(g, action=ACTION.OTPPINCONTENTS,
+                                           user_obj=request.User).action_values(unique=True)
 
     if len(pol_minlen) == 1 and len(pin) < int(list(pol_minlen)[0]):
         # check the minimum length requirement
@@ -320,12 +319,7 @@ def sms_identifiers(request=None, action=None):
     sms_identifier = request.all_data.get("sms.identifier")
     if sms_identifier:
         from privacyidea.lib.tokens.smstoken import SMSACTION
-        if g.logged_in_user.get("role") == ROLE.USER:
-            realm = g.logged_in_user.get("realm")
-        else:
-            realm = getParam(request.all_data, "realm")
-
-        pols = Match.admin_or_user(g, action=SMSACTION.GATEWAYS, realm=realm).action_values(unique=False)
+        pols = Match.admin_or_user(g, action=SMSACTION.GATEWAYS, user_obj=request.User).action_values(unique=False)
         gateway_identifiers = []
 
         for p in pols:
@@ -520,7 +514,7 @@ def twostep_enrollment_activation(request=None, action=None):
     # self-enrolling user).
     # Tokentypes have separate twostep actions
     action = "{}_2step".format(token_type)
-    twostep_enabled_pols = Match.admin_or_user(g, action=action, realm=user_object.realm).action_values(unique=True)
+    twostep_enabled_pols = Match.admin_or_user(g, action=action, user_obj=user_object).action_values(unique=True)
     if twostep_enabled_pols:
         enabled_setting = list(twostep_enabled_pols)[0]
         if enabled_setting == "allow":
@@ -813,8 +807,7 @@ def auditlog_age(request=None, action=None):
     :type action: basestring
     :returns: Always true. Modified the parameter request
     """
-    user_object = request.User
-    audit_age = Match.admin_or_user(g, action=ACTION.AUDIT_AGE, realm=user_object.realm).action_values(unique=True)
+    audit_age = Match.admin_or_user(g, action=ACTION.AUDIT_AGE, user_obj=request.User).action_values(unique=True)
     timelimit = None
     timelimit_s = None
     for aa in audit_age:
@@ -918,7 +911,7 @@ def check_admin_tokenlist(request=None, action=None):
         return True
 
     policy_object = g.policy_object
-    pols = Match.admin(g, action=ACTION.TOKENLIST, realm=None).policies()
+    pols = Match.admin(g, action=ACTION.TOKENLIST).policies()
     pols_at_all = policy_object.list_policies(scope=SCOPE.ADMIN, active=True)
 
     if pols_at_all:
@@ -995,11 +988,18 @@ def check_token_upload(request=None, action=None):
     :param filename:
     :return:
     """
-    params = request.all_data
-    upload_allowed = Match.admin(g, action=ACTION.IMPORT, realm=params.get("realm")).allowed()
+    tokenrealms = request.all_data.get("tokenrealms")
+    upload_allowed = True
+    if tokenrealms:
+        for trealm in tokenrealms.split(","):
+            if not Match.generic(g, action=ACTION.IMPORT, adminuser=g.logged_in_user.get("username"),
+                                           adminrealm=g.logged_in_user.get("realm"), realm=trealm).allowed():
+                upload_allowed = False
+    else:
+        upload_allowed = Match.generic(g, action=ACTION.IMPORT, adminuser=g.logged_in_user.get("username"),
+                                        adminrealm=g.logged_in_user.get("realm")).allowed()
     if not upload_allowed:
-        raise PolicyError("Admin actions are defined, but you are not allowed"
-                          " to upload token files.")
+        raise PolicyError("Admin actions are defined, but you are not allowed to upload token files.")
     return True
 
 
@@ -1320,7 +1320,7 @@ def allowed_audit_realm(request=None, action=None):
     # for admins, as users are only allowed to view their own realm anyway (this
     # is ensured by the fixed "realm" parameter)
     if g.logged_in_user["role"] == ROLE.ADMIN:
-        pols = Match.admin(g, action=ACTION.AUDIT, realm=None).policies()
+        pols = Match.admin(g, action=ACTION.AUDIT).policies()
         if pols:
             # get all values in realm:
             allowed_audit_realms = []
diff --git a/privacyidea/api/token.py b/privacyidea/api/token.py
--- a/privacyidea/api/token.py
+++ b/privacyidea/api/token.py
@@ -885,7 +885,7 @@ def loadtokens_api(filename=None):
     :jsonparam filename: The name of the token file, that is imported
     :jsonparam type: The file type. Can be "aladdin-xml",
         "oathcsv" or "yubikeycsv".
-    :jsonparam tokenrealms: comma separated list of tokens.
+    :jsonparam tokenrealms: comma separated list of realms.
     :jsonparam psk: Pre Shared Key, when importing PSKC
     :return: The number of the imported tokens
     :rtype: int
diff --git a/privacyidea/lib/policy.py b/privacyidea/lib/policy.py
--- a/privacyidea/lib/policy.py
+++ b/privacyidea/lib/policy.py
@@ -2386,57 +2386,60 @@ def user(cls, g, scope, action, user_object):
                    sort_by_priority=True)
 
     @classmethod
-    def admin(cls, g, action, realm):
+    def admin(cls, g, action, user_obj=None):
         """
         Match admin policies with an action and, optionally, a realm.
         Assumes that the currently logged-in user is an admin, and throws an error otherwise.
         Policies will be matched against the admin's username and adminrealm,
-        and optionally also the provided user realm.
+        and optionally also the provided user_obj on which the admin is acting
         The client IP is matched implicitly.
         :param g: context object
         :param action: the policy action
-        :param realm: the user realm against which policies should be matched. Can be None.
-        :type realm: str or None
+        :param user_obj: the user against which policies should be matched. Can be None.
+        :type user_obj: User or None
         :rtype: ``Match``
         """
-        username = g.logged_in_user["username"]
+        adminuser = g.logged_in_user["username"]
         adminrealm = g.logged_in_user["realm"]
         from privacyidea.lib.auth import ROLE
         if g.logged_in_user["role"] != ROLE.ADMIN:
             raise MatchingError("Policies with scope ADMIN can only be retrieved by admins")
-        return cls(g, name=None, scope=SCOPE.ADMIN, realm=realm, active=True,
-                   resolver=None, user=username, user_object=None,
-                   client=g.client_ip, action=action, adminrealm=adminrealm, time=None,
+        return cls(g, name=None, scope=SCOPE.ADMIN, user_object=user_obj, active=True,
+                   resolver=None, client=g.client_ip, action=action,
+                   adminuser=adminuser, adminrealm=adminrealm, time=None,
                    sort_by_priority=True)
 
     @classmethod
-    def admin_or_user(cls, g, action, realm):
+    def admin_or_user(cls, g, action, user_obj):
         """
         Depending on the role of the currently logged-in user, match either scope=ADMIN or scope=USER policies.
         If the currently logged-in user is an admin, match policies against the username, adminrealm
-        and the given user realm.
+        and the given user_obj on which the admin is acting.
         If the currently logged-in user is a user, match policies against the username and the given realm.
         The client IP is matched implicitly.
         :param g: context object
         :param action: the policy action
-        :param realm: the given realm
+        :param user_obj: the user_obj on which the administrator is acting
         :rtype: ``Match``
         """
         from privacyidea.lib.auth import ROLE
-        if g.logged_in_user["role"] == ROLE.ADMIN:
-            scope = SCOPE.ADMIN
-            username = g.logged_in_user["username"]
+        adminrealm = adminuser = username = userrealm = None
+        scope = g.logged_in_user["role"]
+        if scope == ROLE.ADMIN:
+            adminuser = g.logged_in_user["username"]
             adminrealm = g.logged_in_user["realm"]
-        elif g.logged_in_user["role"] == ROLE.USER:
-            scope = SCOPE.USER
-            username = g.logged_in_user["username"]
-            adminrealm = None
+        elif scope == ROLE.USER:
+            if not user_obj:
+                # If we have a user object (including resolver) in a request, we use this on.
+                # Otherwise we take the user from the logged in user.
+                username = g.logged_in_user["username"]
+                userrealm = g.logged_in_user["realm"]
         else:
             raise MatchingError("Unknown role")
-        return cls(g, name=None, scope=scope, realm=realm, active=True,
-                   resolver=None, user=username, user_object=None,
-                   client=g.client_ip, action=action, adminrealm=adminrealm, time=None,
-                   sort_by_priority=True)
+        return cls(g, name=None, scope=scope, realm=userrealm, active=True,
+                   resolver=None, user=username, user_object=user_obj,
+                   client=g.client_ip, action=action, adminrealm=adminrealm, adminuser=adminuser,
+                   time=None, sort_by_priority=True)
 
     @classmethod
     def generic(cls, g, scope=None, realm=None, resolver=None, user=None, user_object=None,


Test patch:
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
@@ -240,7 +240,6 @@ def test_02_check_token_init(self):
         # finally delete policy
         delete_policy("pol1")
 
-
     def test_03_check_token_upload(self):
         g.logged_in_user = {"username": "admin1",
                             "realm": "",
@@ -254,7 +253,7 @@ def test_03_check_token_upload(self):
         g.client_ip = env["REMOTE_ADDR"]
         req = Request(env)
         req.all_data = {"filename": "token.xml"}
-
+        req.User = User()
         # Set a policy, that does allow the action
         set_policy(name="pol1",
                    scope=SCOPE.ADMIN,
@@ -272,6 +271,7 @@ def test_03_check_token_upload(self):
         g.client_ip = env["REMOTE_ADDR"]
         req = Request(env)
         req.all_data = {"filename": "token.xml"}
+        req.User = User()
         self.assertRaises(PolicyError,
                           check_token_upload, req)
         # finally delete policy
@@ -760,6 +760,7 @@ def test_08b_enroll_pin_user(self):
         delete_policy("pol1")
 
     def test_09_pin_policies(self):
+        create_realm("home", [self.resolvername1])
         g.logged_in_user = {"username": "user1",
                             "realm": "",
                             "role": "user"}
@@ -783,6 +784,7 @@ def test_09_pin_policies(self):
         req.all_data = {
                         "user": "cornelius",
                         "realm": "home"}
+        req.User = User("cornelius", "home")
         # The minimum OTP length is 4
         self.assertRaises(PolicyError, check_otp_pin, req)
 
@@ -827,8 +829,10 @@ def test_09_pin_policies(self):
 
         # finally delete policy
         delete_policy("pol1")
+        delete_realm("home")
 
     def test_09_pin_policies_admin(self):
+        create_realm("home", [self.resolvername1])
         g.logged_in_user = {"username": "super",
                             "realm": "",
                             "role": "admin"}
@@ -852,6 +856,7 @@ def test_09_pin_policies_admin(self):
 
         req.all_data = {"user": "cornelius",
                         "realm": "home"}
+        req.User = User("cornelius", "home")
         # The minimum OTP length is 4
         self.assertRaises(PolicyError, check_otp_pin, req)
 
@@ -894,8 +899,10 @@ def test_09_pin_policies_admin(self):
 
         # finally delete policy
         delete_policy("pol1")
+        delete_realm("home")
 
     def test_01b_token_specific_pin_policy(self):
+        create_realm("home", [self.resolvername1])
         g.logged_in_user = {"username": "super",
                             "realm": "",
                             "role": "admin"}
@@ -931,6 +938,7 @@ def test_01b_token_specific_pin_policy(self):
                         "realm": "home",
                         "pin": "123456",
                         "type": "spass"}
+        req.User = User("cornelius", "home")
         # The minimum OTP length is 8
         self.assertRaises(PolicyError, check_otp_pin, req)
 
@@ -951,6 +959,7 @@ def test_01b_token_specific_pin_policy(self):
 
         # finally delete policy
         delete_policy("pol1")
+        delete_realm("home")
 
     def test_10_check_external(self):
         g.logged_in_user = {"username": "user1",
@@ -1485,7 +1494,7 @@ def test_01_sms_identifier(self):
         # Users are allowed to choose gw4
         set_policy("sms1", scope=SCOPE.USER, action="{0!s}=gw4".format(SMSACTION.GATEWAYS))
 
-        g.logged_in_user = {"username": "hans",
+        g.logged_in_user = {"username": "root",
                             "realm": "",
                             "role": "user"}
         builder = EnvironBuilder(method='POST',
@@ -1493,7 +1502,7 @@ def test_01_sms_identifier(self):
                                  headers={})
         env = builder.get_environ()
         req = Request(env)
-        req.User = User()
+        req.User = User("root")
         req.all_data = {"sms.identifier": "gw4"}
         g.policy_object = PolicyClass()
         r = sms_identifiers(req)
diff --git a/tests/test_api_token.py b/tests/test_api_token.py
--- a/tests/test_api_token.py
+++ b/tests/test_api_token.py
@@ -8,11 +8,11 @@
 from privacyidea.lib.policy import (set_policy, delete_policy, SCOPE, ACTION,
                                     enable_policy,
                                     PolicyClass)
-from privacyidea.lib.token import get_tokens, init_token, remove_token, get_tokens_from_serial_or_user, enable_token
+from privacyidea.lib.token import (get_tokens, init_token, remove_token, get_tokens_from_serial_or_user, enable_token,
+                                   check_serial_pass, get_realms_of_token)
 from privacyidea.lib.user import User
 from privacyidea.lib.caconnector import save_caconnector
 from six.moves.urllib.parse import urlencode
-from privacyidea.lib.token import check_serial_pass
 from privacyidea.lib.tokenclass import DATE_FORMAT
 from privacyidea.lib.config import set_privacyidea_config, delete_privacyidea_config
 from dateutil.tz import tzlocal
@@ -933,6 +933,71 @@ def test_11_load_tokens(self):
             value = result.get("value")
             self.assertTrue(value == 1, result)
 
+    def test_11_load_tokens_only_to_specific_realm(self):
+        # Load token to a realm
+        def _clean_up_tokens():
+            remove_token("token01")
+            remove_token("token02")
+            remove_token("token03")
+
+        _clean_up_tokens()
+        with self.app.test_request_context('/token/load/import.oath',
+                                           method="POST",
+                                           data={"type": "oathcsv",
+                                                 "tokenrealms": self.realm1,
+                                                 "file": (IMPORTFILE,
+                                                          "import.oath")},
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            value = result.get("value")
+            self.assertTrue(value == 3, result)
+        # Now check, if the tokens are in the realm
+        from privacyidea.lib.token import get_realms_of_token
+        r = get_realms_of_token("token01")
+        self.assertIn(self.realm1, r)
+
+        # Now set a policy, that allows the admin to upload the tokens into this realm
+        set_policy(name="tokupload", scope=SCOPE.ADMIN, action=ACTION.IMPORT, realm=self.realm1,
+                   adminuser="testadmin")
+        _clean_up_tokens()
+        with self.app.test_request_context('/token/load/import.oath',
+                                           method="POST",
+                                           data={"type": "oathcsv",
+                                                 "tokenrealms": self.realm1,
+                                                 "file": (IMPORTFILE,
+                                                          "import.oath")},
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            value = result.get("value")
+            self.assertTrue(value == 3, result)
+        # Now check, if the tokens are in the realm
+        r = get_realms_of_token("token01")
+        self.assertIn(self.realm1, r)
+
+        # Now define a policy, that allows the user to upload tokens to some other realm
+        set_policy(name="tokupload", scope=SCOPE.ADMIN, action=ACTION.IMPORT, realm="otherrealm",
+                   adminuser="testadmin")
+        _clean_up_tokens()
+        with self.app.test_request_context('/token/load/import.oath',
+                                           method="POST",
+                                           data={"type": "oathcsv",
+                                                 "tokenrealms": self.realm1,
+                                                 "file": (IMPORTFILE,
+                                                          "import.oath")},
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 403, res)
+            result = res.json.get("result")
+            self.assertFalse(result.get("status"))
+            self.assertEqual(u"Admin actions are defined, but you are not allowed to upload token files.",
+                             result.get("error").get("message"))
+
+        delete_policy("tokupload")
+
     def test_12_copy_token(self):
         self._create_temp_token("FROM001")
         self._create_temp_token("TO001")
@@ -1047,7 +1112,7 @@ def test_14_get_serial_by_otp(self):
             self.assertTrue(res.status_code == 200, res)
             result = res.json.get("result")
             value = result.get("value")
-            self.assertEqual(value.get("count"), 28)
+            self.assertEqual(value.get("count"), 25)
             self.assertEqual(value.get("serial"), None)
 
         # multiple tokens are matching!
diff --git a/tests/test_lib_policy.py b/tests/test_lib_policy.py
--- a/tests/test_lib_policy.py
+++ b/tests/test_lib_policy.py
@@ -1411,14 +1411,14 @@ def test_04_admin(self):
 
         self.check_names(Match.admin(g, "enable", None).policies(),
                          {"pol4"})
-        self.check_names(Match.admin(g, "enable", "realm2").policies(),
+        self.check_names(Match.admin(g, "enable", User("cornelius", "realm2")).policies(),
                          {"pol4"})
-        self.check_names(Match.admin(g, "enable", "realm1").policies(),
+        self.check_names(Match.admin(g, "enable", User("cornelius", "realm1")).policies(),
                          {})
 
         g.logged_in_user = {"username": "superroot", "realm": "", "role": ROLE.USER}
         with self.assertRaises(MatchingError):
-            self.check_names(Match.admin(g, "enable", "realm1").policies(),
+            self.check_names(Match.admin(g, "enable", User("cornelius", "realm1")).policies(),
                              {"pol4"})
 
     def test_05_admin_or_user(self):
@@ -1430,18 +1430,21 @@ def test_05_admin_or_user(self):
         g.logged_in_user = {"username": "superroot", "realm": "", "role": ROLE.ADMIN}
         self.check_names(Match.admin_or_user(g, "audit", None).policies(),
                          {"pol4"})
-        self.check_names(Match.admin_or_user(g, "audit", "realm2").policies(),
+        self.check_names(Match.admin_or_user(g, "audit", User("cornelius", "realm2")).policies(),
                          {"pol4"})
-        self.check_names(Match.admin_or_user(g, "audit", "realm1").policies(),
+        self.check_names(Match.admin_or_user(g, "audit", User("cornelius", "realm1")).policies(),
                          {})
 
-        g.logged_in_user = {"username": "foobar", "realm": "asdf", "role": ROLE.USER}
+        g.logged_in_user = {"username": "foobar", "realm": "realm1", "role": ROLE.USER}
+        # The user foobar@realm1 matches pol1
         self.check_names(Match.admin_or_user(g, "audit", None).policies(),
                          {"pol1"})
-        self.check_names(Match.admin_or_user(g, "audit", "realm2").policies(),
+        # A user in a different realm does not match!
+        self.check_names(Match.admin_or_user(g, "audit", User("cornelius", "realm2")).policies(),
+                         {})
+        # A wrong user in a realm does not match!
+        self.check_names(Match.admin_or_user(g, "audit", User("cornelius", "realm1")).policies(),
                          {})
-        self.check_names(Match.admin_or_user(g, "audit", "realm1").policies(),
-                         {"pol1"})
 
         g.logged_in_user = {"username": "baz", "realm": "asdf", "role": ROLE.USER}
         self.check_names(Match.admin_or_user(g, "audit", None).policies(),
@@ -1449,7 +1452,7 @@ def test_05_admin_or_user(self):
 
         g.logged_in_user = {"username": "baz", "realm": "asdf", "role": "something"}
         with self.assertRaises(MatchingError):
-            self.check_names(Match.admin_or_user(g, "enable", "realm1").policies(),
+            self.check_names(Match.admin_or_user(g, "enable", User("cornelius", "realm1")).policies(),
                              {"pol4"})
 
     @classmethod