Repository: privacyidea/privacyidea

Issue:
Tokenhandler should delete tokeninfo
An additional action in the token handler should be to delete a tokeninfo field.
This action would only require the ``tokeninfo_key``.

See https://community.privacyidea.org/t/how-to-clear-validity-period-on-privacyidea-once-set-through-the-ui-interface/667


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
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 #
+#  2018-34-14 Paul Lettich <paul.lettich@netknights.it>
+#             Add "delete tokeninfo" action
 #  2017-07-18 Cornelius Kölbel <cornelius.koelbel@netknights.it>
 #             Allow setting time with timedelta
 #  2017-01-21 Cornelius Kölbel <cornelius.koelbel@netknights.it>
@@ -41,7 +43,7 @@
 from privacyidea.lib.token import (set_realms, remove_token, enable_token,
                                    unassign_token, init_token, set_description,
                                    set_count_window, add_tokeninfo,
-                                   set_failcounter)
+                                   set_failcounter, delete_tokeninfo)
 from privacyidea.lib.utils import (parse_date, is_true,
                                    parse_time_offset_from_now)
 from privacyidea.lib.tokenclass import DATE_FORMAT, AUTH_DATE_FORMAT
@@ -70,13 +72,14 @@ class ACTION_TYPE(object):
     SET_COUNTWINDOW = "set countwindow"
     SET_TOKENINFO = "set tokeninfo"
     SET_FAILCOUNTER = "set failcounter"
+    DELETE_TOKENINFO = "delete tokeninfo"
 
 
 class VALIDITY(object):
     """
     Allowed validity options
     """
-    START= "valid from"
+    START = "valid from"
     END = "valid till"
 
 
@@ -211,6 +214,14 @@ def actions(cls):
                                 "description": _("Set the above key the this "
                                                  "value.")
                             }
+                       },
+                   ACTION_TYPE.DELETE_TOKENINFO:
+                       {"key":
+                            {
+                                "type": "str",
+                                "required": True,
+                                "description": _("Delete this tokeninfo key.")
+                            }
                        }
                    }
         return actions
@@ -244,7 +255,8 @@ def do(self, action, options=None):
                               ACTION_TYPE.SET_VALIDITY,
                               ACTION_TYPE.SET_COUNTWINDOW,
                               ACTION_TYPE.SET_TOKENINFO,
-                              ACTION_TYPE.SET_FAILCOUNTER]:
+                              ACTION_TYPE.SET_FAILCOUNTER,
+                              ACTION_TYPE.DELETE_TOKENINFO]:
             if serial:
                 log.info("{0!s} for token {1!s}".format(action, serial))
                 if action.lower() == ACTION_TYPE.SET_TOKENREALM:
@@ -299,6 +311,8 @@ def do(self, action, options=None):
                                       realm=realm,
                                       ua_browser=request.user_agent.browser,
                                       ua_string=request.user_agent.string))
+                elif action.lower() == ACTION_TYPE.DELETE_TOKENINFO:
+                    delete_tokeninfo(serial, handler_options.get("key"))
                 elif action.lower() == ACTION_TYPE.SET_VALIDITY:
                     start_date = handler_options.get(VALIDITY.START)
                     end_date = handler_options.get(VALIDITY.END)
@@ -354,4 +368,3 @@ def do(self, action, options=None):
             log.info("New token {0!s} enrolled.".format(t.token.serial))
 
         return ret
-


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
@@ -936,7 +936,7 @@ def test_04_unassign(self):
         resp = Response()
         resp.data = """{"result": {"value": true}}"""
 
-        # Now the initiailized token will be set in realm2
+        # Now the initialized token will be set in realm2
         options = {"g": g,
                    "request": req,
                    "response": resp,
@@ -1181,7 +1181,7 @@ def test_07_set_validity(self):
                    "response": resp,
                    "handler_def": {"options": {VALIDITY.START: "+10m",
                                                VALIDITY.END: "+10d"}
-                   }
+                                   }
                    }
 
         t_handler = TokenEventHandler()
@@ -1249,7 +1249,7 @@ def test_08_set_count_window(self):
 
         remove_token("SPASS01")
 
-    def test_09_set_tokeninfo(self):
+    def test_09_set_delete_tokeninfo(self):
         # setup realms
         self.setUp_user_realms()
 
@@ -1316,6 +1316,37 @@ def test_09_set_tokeninfo(self):
         tw = t[0].get_tokeninfo("pastText")
         self.assertTrue(tw.startswith("it was 20"))
         self.assertTrue(tw.endswith("0..."))
+        ti0 = t[0].get_tokeninfo()
+
+        # Delete non existing token
+        options = {"g": g,
+                   "request": req,
+                   "response": resp,
+                   "handler_def": {"options": {"key": "SomeNonExistingKey"}
+                                   }
+                   }
+        t_handler = TokenEventHandler()
+        res = t_handler.do(ACTION_TYPE.DELETE_TOKENINFO, options=options)
+        self.assertTrue(res)
+        # Check if the token info was deleted
+        t = get_tokens(serial="SPASS01")
+        ti1 = t[0].get_tokeninfo()
+        self.assertEqual(ti0, ti1)
+
+        # Delete token info "pastText"
+        options = {"g": g,
+                   "request": req,
+                   "response": resp,
+                   "handler_def": {"options": {"key": "pastText"}
+                                   }
+                   }
+        t_handler = TokenEventHandler()
+        res = t_handler.do(ACTION_TYPE.DELETE_TOKENINFO, options=options)
+        self.assertTrue(res)
+        # Check if the token info was deleted
+        t = get_tokens(serial="SPASS01")
+        tw = t[0].get_tokeninfo("pastText", "key not found")
+        self.assertEqual(tw, "key not found")
 
         remove_token("SPASS01")