Repository: privacyidea/privacyidea

Issue:
Honor reset_all_user_tokens also for challenge response
If the authentication is a successful response to a challenge, the policy ``reset_all_user_tokens`` is currently not used.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/policydecorators.py b/privacyidea/lib/policydecorators.py
--- a/privacyidea/lib/policydecorators.py
+++ b/privacyidea/lib/policydecorators.py
@@ -677,3 +677,45 @@ def config_lost_token(wrapped_function, *args, **kwds):
 
     return wrapped_function(*args, **kwds)
 
+
+def reset_all_user_tokens(wrapped_function, *args, **kwds):
+    """
+    Resets all tokens if the corresponding policy is set.
+
+    :param token: The successful token, the tokenowner is used to find policies.
+    :param tokenobject_list: The list of all the tokens of the user
+    :param options: options dictionary containing g.
+    :return: None
+    """
+    tokenobject_list = args[0]
+    options = kwds.get("options") or {}
+    g = options.get("g")
+    allow_reset = kwds.get("allow_reset_all_tokens")
+
+    r = wrapped_function(*args, **kwds)
+
+    # A successful authentication was done
+    if r[0] and g and allow_reset:
+        clientip = options.get("clientip")
+        policy_object = g.policy_object
+        token_owner = tokenobject_list[0].user
+        reset_all = policy_object.get_policies(
+            action=ACTION.RESETALLTOKENS,
+            scope=SCOPE.AUTH,
+            realm=token_owner.login if token_owner else None,
+            resolver=token_owner.resolver if token_owner else None,
+            user=token_owner.realm if token_owner else None,
+            client=clientip, active=True,
+            audit_data=g.audit_object.audit_data)
+        if reset_all:
+            log.debug("Reset failcounter of all tokens of {0!s}".format(
+                token_owner))
+            for tok_obj_reset in tokenobject_list:
+                try:
+                    tok_obj_reset.reset()
+                except Exception:
+                    log.debug(
+                        "registration token does not exist anymore and "
+                        "cannot be reset.")
+
+    return r
\ No newline at end of file
diff --git a/privacyidea/lib/token.py b/privacyidea/lib/token.py
--- a/privacyidea/lib/token.py
+++ b/privacyidea/lib/token.py
@@ -95,7 +95,8 @@
                                               auth_user_timelimit,
                                               auth_lastauth,
                                               auth_cache,
-                                              config_lost_token)
+                                              config_lost_token,
+                                              reset_all_user_tokens)
 from privacyidea.lib.tokenclass import DATE_FORMAT
 from privacyidea.lib.tokenclass import TOKENKIND
 from dateutil.tz import tzlocal
@@ -1894,7 +1895,8 @@ def check_realm_pass(realm, passw, options=None):
                                 "this realm"
     else:
         res, reply_dict = check_token_list(tokenobject_list, passw,
-                                           options=options)
+                                           options=options,
+                                           allow_reset_all_tokens=False)
     return res, reply_dict
 
 
@@ -1922,7 +1924,8 @@ def check_serial_pass(serial, passw, options=None):
     tokenobject = get_one_token(serial=serial)
     res, reply_dict = check_token_list([tokenobject], passw,
                                        user=tokenobject.user,
-                                       options=options)
+                                       options=options,
+                                       allow_reset_all_tokens=True)
 
     return res, reply_dict
 
@@ -1976,13 +1979,15 @@ def check_user_pass(user, passw, options=None):
         tokenobject = tokenobject_list[0]
         res, reply_dict = check_token_list(tokenobject_list, passw,
                                            user=tokenobject.user,
-                                           options=options)
+                                           options=options,
+                                           allow_reset_all_tokens=True)
 
     return res, reply_dict
 
 
 @log_with(log)
-def check_token_list(tokenobject_list, passw, user=None, options=None):
+@libpolicy(reset_all_user_tokens)
+def check_token_list(tokenobject_list, passw, user=None, options=None, allow_reset_all_tokens=False):
     """
     this takes a list of token objects and tries to find the matching token
     for the given passw. It also tests,
@@ -1997,6 +2002,8 @@ def check_token_list(tokenobject_list, passw, user=None, options=None):
     :param passw: the provided passw (mostly pin+otp)
     :param user: the identified use - as class object
     :param options: additional parameters, which are passed to the token
+    :param allow_reset_all_tokens: If set to True, the policy reset_all_user_tokens is evaluated to
+        reset all user tokens accordingly. Note: This parameter is used in the decorator.
 
     :return: tuple of success and optional response
     :rtype: (bool, dict)
@@ -2135,30 +2142,6 @@ def check_token_list(tokenobject_list, passw, user=None, options=None):
                     0].is_pin_change(password=True)
         reply_dict["message"] = ", ".join(message_list)
 
-        # Check if we should reset ALL tokens of the user.
-        g = options.get("g")
-        if g:
-            clientip = options.get("clientip")
-            policy_object = g.policy_object
-            token_owner = valid_token_list[0].user
-            reset_all = policy_object.get_policies(
-                action=ACTION.RESETALLTOKENS,
-                scope=SCOPE.AUTH,
-                realm=token_owner.login if token_owner else None,
-                user=token_owner.realm if token_owner else None,
-                client=clientip, active=True,
-                audit_data=g.audit_object.audit_data)
-            if reset_all:
-                log.debug("Reset failcounter of all tokens of {0!s}".format(
-                    token_owner))
-                for tok_obj_reset in tokenobject_list:
-                    try:
-                        tok_obj_reset.reset()
-                    except Exception:
-                        log.debug(
-                            "registration token does not exist anymore and "
-                            "cannot be reset.")
-
     elif challenge_response_token_list:
         # The RESPONSE for a previous request of a challenge response token was
         # found.
diff --git a/privacyidea/lib/tokens/yubikeytoken.py b/privacyidea/lib/tokens/yubikeytoken.py
--- a/privacyidea/lib/tokens/yubikeytoken.py
+++ b/privacyidea/lib/tokens/yubikeytoken.py
@@ -450,7 +450,7 @@ def check_yubikey_pass(passw):
                                     prefix))
             return res, opt
 
-        (res, opt) = check_token_list(token_list, passw)
+        (res, opt) = check_token_list(token_list, passw, allow_reset_all_tokens=True)
         return res, opt
 
     @log_with(log)


Test patch:
diff --git a/tests/test_lib_policydecorator.py b/tests/test_lib_policydecorator.py
--- a/tests/test_lib_policydecorator.py
+++ b/tests/test_lib_policydecorator.py
@@ -19,7 +19,7 @@
                                               login_mode, config_lost_token,
                                               challenge_response_allowed,
                                               auth_user_timelimit, auth_cache,
-                                              auth_lastauth)
+                                              auth_lastauth, reset_all_user_tokens)
 from privacyidea.lib.user import User
 from privacyidea.lib.resolver import save_resolver, delete_resolver
 from privacyidea.lib.realm import set_realm, delete_realm
@@ -27,6 +27,7 @@
                                    get_tokens)
 from privacyidea.lib.error import UserError, PolicyError
 from privacyidea.lib.radiusserver import add_radius
+from flask import g
 import datetime
 from . import radiusmock
 import binascii
@@ -56,6 +57,11 @@ class LibPolicyTestCase(MyTestCase):
     def fake_check_otp(dummy, pin, user=None, options=None):
         return pin == "FAKE"
 
+    @staticmethod
+    def fake_check_token_list(tokenobject_list, passw, user=None, options=None, allow_reset_all_tokens=True,
+                              result=False):
+        return result, "some text"
+
     def test_01_otppin(self):
         my_user = User("cornelius", realm="r1")
         set_policy(name="pol1",
@@ -779,3 +785,41 @@ def test_14_otppin_priority(self):
         self.assertFalse(r)
         delete_policy("pol1")
         delete_policy("pol2")
+
+    def test_15_reset_all_failcounters(self):
+        self.setUp_user_realms()
+
+        set_policy("reset_all", scope=SCOPE.AUTH,
+                   action=ACTION.RESETALLTOKENS)
+
+        user = User(login="cornelius", realm=self.realm1)
+        pin1 = "pin1"
+        pin2 = "pin2"
+        token1 = init_token({"serial": pin1, "pin": pin1,
+                             "type": "spass"}, user=user)
+        token2 = init_token({"serial": pin2, "pin": pin2,
+                             "type": "spass"}, user=user)
+
+        token1.inc_failcount()
+        token2.inc_failcount()
+        token2.inc_failcount()
+        self.assertEqual(token1.token.failcount, 1)
+        self.assertEqual(token2.token.failcount, 2)
+
+        g.policy_object = PolicyClass()
+        options = {"g": g}
+
+        r = reset_all_user_tokens(self.fake_check_token_list,
+                                  [token1, token2],
+                                  "pw", None,
+                                  options=options,
+                                  allow_reset_all_tokens=True,
+                                  result=True)
+        self.assertTrue(r)
+
+        self.assertEqual(token1.token.failcount, 0)
+        self.assertEqual(token2.token.failcount, 0)
+
+        # Clean up
+        remove_token(pin1)
+        remove_token(pin2)
diff --git a/tests/test_lib_token.py b/tests/test_lib_token.py
--- a/tests/test_lib_token.py
+++ b/tests/test_lib_token.py
@@ -1530,7 +1530,7 @@ def test_04_reset_all_failcounters(self):
         options = {"g": g}
 
         check_token_list([token1, token2], pin1, user=user,
-                         options=options)
+                         options=options, allow_reset_all_tokens=True)
 
         self.assertEqual(token1.token.failcount, 0)
         self.assertEqual(token2.token.failcount, 0)
@@ -1547,7 +1547,8 @@ def test_04_reset_all_failcounters(self):
         self.assertEqual(token1.token.failcount, 1)
         self.assertEqual(token2.token.failcount, 2)
 
-        check_token_list([token1, token2], pin1, options=options)
+        check_token_list([token1, token2], pin1, options=options,
+                         allow_reset_all_tokens=True)
 
         self.assertEqual(token1.token.failcount, 0)
         self.assertEqual(token2.token.failcount, 0)