Repository: privacyidea/privacyidea

Issue:
Event Handler: Increase or decrease failcounter
Allow Token-Event-Handler to not only **set failcounter** but also to increment or decrease failcounter.

This could be done by entering +1 or -1 do increase or decrease the failcounter.
We could then parse for "+" or "-" and act accordingly.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/eventhandler/tokenhandler.py b/privacyidea/lib/eventhandler/tokenhandler.py
--- a/privacyidea/lib/eventhandler/tokenhandler.py
+++ b/privacyidea/lib/eventhandler/tokenhandler.py
@@ -46,7 +46,8 @@
 from privacyidea.lib.token import (set_realms, remove_token, enable_token,
                                    unassign_token, init_token, set_description,
                                    set_count_window, add_tokeninfo,
-                                   set_failcounter, delete_tokeninfo)
+                                   set_failcounter, delete_tokeninfo,
+                                   get_one_token)
 from privacyidea.lib.utils import (parse_date, is_true,
                                    parse_time_offset_from_now)
 from privacyidea.lib.tokenclass import DATE_FORMAT, AUTH_DATE_FORMAT
@@ -76,6 +77,7 @@ class ACTION_TYPE(object):
     SET_COUNTWINDOW = "set countwindow"
     SET_TOKENINFO = "set tokeninfo"
     SET_FAILCOUNTER = "set failcounter"
+    CHANGE_FAILCOUNTER = "change failcounter"
     DELETE_TOKENINFO = "delete tokeninfo"
     SET_RANDOM_PIN = "set random pin"
 
@@ -239,6 +241,16 @@ def actions(cls):
                                                     "the token.")
                                }
                        },
+                   ACTION_TYPE.CHANGE_FAILCOUNTER:
+                       {
+                           "change fail counter":
+                               {
+                                   "type": "str",
+                                   "required": True,
+                                   "description": _("Increase or decrease the fail counter of the token. "
+                                                    "Values of +n, -n with n being an integer are accepted.")
+                               }
+                       },
                    ACTION_TYPE.SET_TOKENINFO:
                        {"key":
                            {
@@ -294,6 +306,7 @@ def do(self, action, options=None):
                               ACTION_TYPE.SET_COUNTWINDOW,
                               ACTION_TYPE.SET_TOKENINFO,
                               ACTION_TYPE.SET_FAILCOUNTER,
+                              ACTION_TYPE.CHANGE_FAILCOUNTER,
                               ACTION_TYPE.SET_RANDOM_PIN,
                               ACTION_TYPE.DELETE_TOKENINFO]:
             if serial:
@@ -377,6 +390,14 @@ def do(self, action, options=None):
                     except Exception as exx:
                         log.warning("Misconfiguration: Failed to set fail "
                                     "counter!")
+                elif action.lower() == ACTION_TYPE.CHANGE_FAILCOUNTER:
+                    try:
+                        token_obj = get_one_token(serial=serial)
+                        token_obj.set_failcount(
+                            token_obj.token.failcount + int(handler_options.get("change fail counter")))
+                    except Exception as exx:
+                        log.warning("Misconfiguration: Failed to increase or decrease fail "
+                                    "counter!")
             else:
                 log.info("Action {0!s} requires serial number. But no serial "
                          "number could be found in request.")


Test patch:
diff --git a/tests/test_lib_events.py b/tests/test_lib_events.py
--- a/tests/test_lib_events.py
+++ b/tests/test_lib_events.py
@@ -39,7 +39,7 @@
 from dateutil.parser import parse as parse_date_string
 from dateutil.tz import tzlocal
 from privacyidea.app import PiResponseClass as Response
-
+from collections import OrderedDict
 
 class EventHandlerLibTestCase(MyTestCase):
 
@@ -2156,7 +2156,7 @@ def test_09_set_delete_tokeninfo(self):
 
         remove_token("SPASS01")
 
-    def test_10_set_failcounter(self):
+    def test_10_set_or_change_failcounter(self):
         # setup realms
         self.setUp_user_realms()
 
@@ -2188,7 +2188,7 @@ def test_10_set_failcounter(self):
         resp = Response()
         resp.data = """{"result": {"value": true}}"""
 
-        # The token faile counter will be set to 7
+        # The token fail counter will be set to 7
         options = {"g": g,
                    "request": req,
                    "response": resp,
@@ -2198,11 +2198,24 @@ def test_10_set_failcounter(self):
         t_handler = TokenEventHandler()
         res = t_handler.do(ACTION_TYPE.SET_FAILCOUNTER, options=options)
         self.assertTrue(res)
-        # Check if the token has the correct sync window
+        # Check if the token has the correct fail counter
         t = get_tokens(serial="SPASS01")
         tw = t[0].get_failcount()
         self.assertEqual(tw, 7)
 
+        # check the change failcount option starting with the set failcount of 7
+        handler_options = OrderedDict([("-8", -1),
+                                       ("2", 1),
+                                       ("+1", 2)])
+        for diff, failcount in handler_options.items():
+            options["handler_def"] = {"options": {"change fail counter": diff}}
+            res = t_handler.do(ACTION_TYPE.CHANGE_FAILCOUNTER, options=options)
+            self.assertTrue(res)
+            # Check if the token has the correct fail counter
+            t = get_tokens(serial="SPASS01")
+            tw = t[0].get_failcount()
+            self.assertEqual(tw, failcount)
+
         remove_token("SPASS01")
 
     def test_11_set_random_pin(self):