Repository: privacyidea/privacyidea

Issue:
Allow event handler to access token serial during import
**Is your feature request related to a problem? Please describe.**

When importing tokens we should make the serial numbers of the imported tokens available so that the event handler can act on all imported tokens.



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
@@ -329,103 +329,108 @@ def do(self, action, options=None):
                               ACTION_TYPE.SET_RANDOM_PIN,
                               ACTION_TYPE.DELETE_TOKENINFO]:
             if serial:
-                log.info("{0!s} for token {1!s}".format(action, serial))
-                if action.lower() == ACTION_TYPE.SET_TOKENREALM:
-                    realm = handler_options.get("realm")
-                    only_realm = is_true(handler_options.get("only_realm"))
-                    # Set the realm..
-                    log.info("Setting realm of token {0!s} to {1!s}".format(
-                        serial, realm))
-                    # Add the token realm
-                    set_realms(serial, [realm], add=not only_realm)
-                elif action.lower() == ACTION_TYPE.SET_RANDOM_PIN:
-                    # If for any reason we have no value, we default to 6
-                    length = int(handler_options.get("length") or 6)
-                    pin = generate_password(size=length)
-                    if set_pin(serial, pin):
-                        content.setdefault("detail", {})["pin"] = pin
-                        options.get("response").data = json.dumps(content)
-                elif action.lower() == ACTION_TYPE.DELETE:
-                    remove_token(serial=serial)
-                elif action.lower() == ACTION_TYPE.DISABLE:
-                    enable_token(serial, enable=False)
-                elif action.lower() == ACTION_TYPE.ENABLE:
-                    enable_token(serial, enable=True)
-                elif action.lower() == ACTION_TYPE.UNASSIGN:
-                    unassign_token(serial)
-                elif action.lower() == ACTION_TYPE.SET_DESCRIPTION:
-                    description = handler_options.get("description") or ""
-                    description, td = parse_time_offset_from_now(description)
-                    s_now = (datetime.datetime.now(tzlocal()) + td).strftime(
-                        AUTH_DATE_FORMAT)
-                    set_description(serial,
-                                    description.format(
-                                        current_time=s_now,
-                                        now=s_now,
-                                        client_ip=g.client_ip,
-                                        ua_browser=request.user_agent.browser,
-                                        ua_string=request.user_agent.string))
-                elif action.lower() == ACTION_TYPE.SET_COUNTWINDOW:
-                    set_count_window(serial,
-                                     int(handler_options.get("count window",
-                                                             50)))
-                elif action.lower() == ACTION_TYPE.SET_TOKENINFO:
-                    tokeninfo = handler_options.get("value") or ""
-                    tokeninfo, td = parse_time_offset_from_now(tokeninfo)
-                    s_now = (datetime.datetime.now(tzlocal()) + td).strftime(
-                        AUTH_DATE_FORMAT)
-                    try:
-                        username = request.User.loginname
-                        realm = request.User.realm
-                    except Exception:
-                        username = "N/A"
-                        realm = "N/A"
-                    add_tokeninfo(serial, handler_options.get("key"),
-                                  tokeninfo.format(
-                                      current_time=s_now,
-                                      now=s_now,
-                                      client_ip=g.client_ip,
-                                      username=username,
-                                      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)
-                    if start_date:
-                         d = parse_date(start_date)
-                         set_validity_period_start(serial, None,
-                                                   d.strftime(DATE_FORMAT))
-                    if end_date:
-                        d = parse_date(end_date)
-                        set_validity_period_end(serial, None,
-                                                d.strftime(DATE_FORMAT))
-                elif action.lower() == ACTION_TYPE.SET_FAILCOUNTER:
-                    try:
-                        set_failcounter(serial,
-                                        int(handler_options.get("fail counter")))
-                    except Exception as exx:
-                        log.warning("Misconfiguration: Failed to set fail "
-                                    "counter!")
-                elif action.lower() == ACTION_TYPE.SET_MAXFAIL:
-                    try:
-                        set_max_failcount(serial,
-                                          int(handler_options.get("max failcount")))
-                    except Exception as exx:
-                        log.warning("Misconfiguration: Failed to set max failcount!")
-                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.")
+                if ',' in serial:
+                    serials = [t.strip() for t in serial.split(',')]
+                else:
+                    serials = [serial]
+                for serial in serials:
+                    log.info("{0!s} for token {1!s}".format(action, serial))
+                    if action.lower() == ACTION_TYPE.SET_TOKENREALM:
+                        realm = handler_options.get("realm")
+                        only_realm = is_true(handler_options.get("only_realm"))
+                        # Set the realm..
+                        log.info("Setting realm of token {0!s} to {1!s}".format(
+                            serial, realm))
+                        # Add the token realm
+                        set_realms(serial, [realm], add=not only_realm)
+                    elif action.lower() == ACTION_TYPE.SET_RANDOM_PIN:
+                        # If for any reason we have no value, we default to 6
+                        length = int(handler_options.get("length") or 6)
+                        pin = generate_password(size=length)
+                        if set_pin(serial, pin):
+                            content.setdefault("detail", {})["pin"] = pin
+                            options.get("response").data = json.dumps(content)
+                    elif action.lower() == ACTION_TYPE.DELETE:
+                        remove_token(serial=serial)
+                    elif action.lower() == ACTION_TYPE.DISABLE:
+                        enable_token(serial, enable=False)
+                    elif action.lower() == ACTION_TYPE.ENABLE:
+                        enable_token(serial, enable=True)
+                    elif action.lower() == ACTION_TYPE.UNASSIGN:
+                        unassign_token(serial)
+                    elif action.lower() == ACTION_TYPE.SET_DESCRIPTION:
+                        description = handler_options.get("description") or ""
+                        description, td = parse_time_offset_from_now(description)
+                        s_now = (datetime.datetime.now(tzlocal()) + td).strftime(
+                            AUTH_DATE_FORMAT)
+                        set_description(serial,
+                                        description.format(
+                                            current_time=s_now,
+                                            now=s_now,
+                                            client_ip=g.client_ip,
+                                            ua_browser=request.user_agent.browser,
+                                            ua_string=request.user_agent.string))
+                    elif action.lower() == ACTION_TYPE.SET_COUNTWINDOW:
+                        set_count_window(serial,
+                                         int(handler_options.get("count window",
+                                                                 50)))
+                    elif action.lower() == ACTION_TYPE.SET_TOKENINFO:
+                        tokeninfo = handler_options.get("value") or ""
+                        tokeninfo, td = parse_time_offset_from_now(tokeninfo)
+                        s_now = (datetime.datetime.now(tzlocal()) + td).strftime(
+                            AUTH_DATE_FORMAT)
+                        try:
+                            username = request.User.loginname
+                            realm = request.User.realm
+                        except Exception:
+                            username = "N/A"
+                            realm = "N/A"
+                        add_tokeninfo(serial, handler_options.get("key"),
+                                      tokeninfo.format(
+                                          current_time=s_now,
+                                          now=s_now,
+                                          client_ip=g.client_ip,
+                                          username=username,
+                                          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)
+                        if start_date:
+                            d = parse_date(start_date)
+                            set_validity_period_start(serial, None,
+                                                      d.strftime(DATE_FORMAT))
+                        if end_date:
+                            d = parse_date(end_date)
+                            set_validity_period_end(serial, None,
+                                                    d.strftime(DATE_FORMAT))
+                    elif action.lower() == ACTION_TYPE.SET_FAILCOUNTER:
+                        try:
+                            set_failcounter(serial,
+                                            int(handler_options.get("fail counter")))
+                        except Exception as exx:
+                            log.warning("Misconfiguration: Failed to set fail "
+                                        "counter!")
+                    elif action.lower() == ACTION_TYPE.SET_MAXFAIL:
+                        try:
+                            set_max_failcount(serial,
+                                              int(handler_options.get("max failcount")))
+                        except Exception as exx:
+                            log.warning("Misconfiguration: Failed to set max failcount!")
+                    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.")
 
         if action.lower() == ACTION_TYPE.INIT:
             log.info("Initializing new token")


Test patch:
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
@@ -12,6 +12,7 @@
 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.event import set_event, delete_event, EventConfiguration
 from privacyidea.lib.caconnector import save_caconnector
 from six.moves.urllib.parse import urlencode
 from privacyidea.lib.tokenclass import DATE_FORMAT
@@ -26,6 +27,7 @@
 IMPORTPSKC_PASS = "tests/testdata/pskc-password.xml"
 PSK_HEX = "12345678901234567890123456789012"
 YUBICOFILE = "tests/testdata/yubico-oath.csv"
+YUBICOFILE_LONG = "tests/testdata/yubico-oath-long.csv"
 OTPKEY = "3132333435363738393031323334353637383930"
 OTPKEY2 = "010fe88d31948c0c2e3258a4b0f7b11956a258ef"
 CAKEY = "cakey.pem"
@@ -1106,6 +1108,38 @@ def test_11_load_tokens(self):
             value = result.get("value")['n_imported']
             self.assertTrue(value == 1, result)
 
+    def test_11_load_tokens_tokenhandler(self):
+
+        # create a new event to disable tokens after import
+        r = set_event("token_disable", ["token_load"], "Token",
+                      "disable", position="post")
+        events = EventConfiguration()
+        event_id = [event['id'] for event in events.events if event['name'] == 'token_disable'][0]
+
+        # Load yubico.csv
+        with self.app.test_request_context('/token/load/yubico.csv',
+                                            method="POST",
+                                            data={"type": "yubikeycsv",
+                                                  "tokenrealms": self.realm1,
+                                                  "file": (YUBICOFILE_LONG,
+                                                           "yubico.csv")},
+                                            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")['n_imported']
+            self.assertTrue(value == 100, result)
+
+        # check if imported tokens were disabled by event handler
+        tokenobject_list = get_tokens(serial_wildcard="UBOM*", active=False)
+        self.assertEqual(len(tokenobject_list), 100)
+
+        # remove tokens
+        for tok in tokenobject_list:
+            remove_token(serial=tok.token.serial)
+        # remove event
+        delete_event(event_id)
+
     def test_11_load_tokens_only_to_specific_realm(self):
         # Load token to a realm
         def _clean_up_tokens():
diff --git a/tests/testdata/yubico-oath-long.csv b/tests/testdata/yubico-oath-long.csv
new file mode 100644
--- /dev/null
+++ b/tests/testdata/yubico-oath-long.csv
@@ -0,0 +1,100 @@
+1,,0,0,000000000000,2021-07-20T12:42:35,
+2,,0,0,000000000000,2021-07-20T12:42:35,
+3,,0,0,000000000000,2021-07-20T12:42:35,
+4,,0,0,000000000000,2021-07-20T12:42:35,
+5,,0,0,000000000000,2021-07-20T12:42:35,
+6,,0,0,000000000000,2021-07-20T12:42:35,
+7,,0,0,000000000000,2021-07-20T12:42:35,
+8,,0,0,000000000000,2021-07-20T12:42:35,
+9,,0,0,000000000000,2021-07-20T12:42:35,
+10,,0,0,000000000000,2021-07-20T12:42:35,
+11,,0,0,000000000000,2021-07-20T12:42:35,
+12,,0,0,000000000000,2021-07-20T12:42:35,
+13,,0,0,000000000000,2021-07-20T12:42:35,
+14,,0,0,000000000000,2021-07-20T12:42:35,
+15,,0,0,000000000000,2021-07-20T12:42:35,
+16,,0,0,000000000000,2021-07-20T12:42:35,
+17,,0,0,000000000000,2021-07-20T12:42:35,
+18,,0,0,000000000000,2021-07-20T12:42:35,
+19,,0,0,000000000000,2021-07-20T12:42:35,
+20,,0,0,000000000000,2021-07-20T12:42:35,
+21,,0,0,000000000000,2021-07-20T12:42:35,
+22,,0,0,000000000000,2021-07-20T12:42:35,
+23,,0,0,000000000000,2021-07-20T12:42:35,
+24,,0,0,000000000000,2021-07-20T12:42:35,
+25,,0,0,000000000000,2021-07-20T12:42:35,
+26,,0,0,000000000000,2021-07-20T12:42:35,
+27,,0,0,000000000000,2021-07-20T12:42:35,
+28,,0,0,000000000000,2021-07-20T12:42:35,
+29,,0,0,000000000000,2021-07-20T12:42:35,
+30,,0,0,000000000000,2021-07-20T12:42:35,
+31,,0,0,000000000000,2021-07-20T12:42:35,
+32,,0,0,000000000000,2021-07-20T12:42:35,
+33,,0,0,000000000000,2021-07-20T12:42:35,
+34,,0,0,000000000000,2021-07-20T12:42:35,
+35,,0,0,000000000000,2021-07-20T12:42:35,
+36,,0,0,000000000000,2021-07-20T12:42:35,
+37,,0,0,000000000000,2021-07-20T12:42:35,
+38,,0,0,000000000000,2021-07-20T12:42:35,
+39,,0,0,000000000000,2021-07-20T12:42:35,
+40,,0,0,000000000000,2021-07-20T12:42:35,
+41,,0,0,000000000000,2021-07-20T12:42:35,
+42,,0,0,000000000000,2021-07-20T12:42:35,
+43,,0,0,000000000000,2021-07-20T12:42:35,
+44,,0,0,000000000000,2021-07-20T12:42:35,
+45,,0,0,000000000000,2021-07-20T12:42:35,
+46,,0,0,000000000000,2021-07-20T12:42:35,
+47,,0,0,000000000000,2021-07-20T12:42:35,
+48,,0,0,000000000000,2021-07-20T12:42:35,
+49,,0,0,000000000000,2021-07-20T12:42:35,
+50,,0,0,000000000000,2021-07-20T12:42:35,
+51,,0,0,000000000000,2021-07-20T12:42:35,
+52,,0,0,000000000000,2021-07-20T12:42:35,
+53,,0,0,000000000000,2021-07-20T12:42:35,
+54,,0,0,000000000000,2021-07-20T12:42:35,
+55,,0,0,000000000000,2021-07-20T12:42:35,
+56,,0,0,000000000000,2021-07-20T12:42:35,
+57,,0,0,000000000000,2021-07-20T12:42:35,
+58,,0,0,000000000000,2021-07-20T12:42:35,
+59,,0,0,000000000000,2021-07-20T12:42:35,
+60,,0,0,000000000000,2021-07-20T12:42:35,
+61,,0,0,000000000000,2021-07-20T12:42:35,
+62,,0,0,000000000000,2021-07-20T12:42:35,
+63,,0,0,000000000000,2021-07-20T12:42:35,
+64,,0,0,000000000000,2021-07-20T12:42:35,
+65,,0,0,000000000000,2021-07-20T12:42:35,
+66,,0,0,000000000000,2021-07-20T12:42:35,
+67,,0,0,000000000000,2021-07-20T12:42:35,
+68,,0,0,000000000000,2021-07-20T12:42:35,
+69,,0,0,000000000000,2021-07-20T12:42:35,
+70,,0,0,000000000000,2021-07-20T12:42:35,
+71,,0,0,000000000000,2021-07-20T12:42:35,
+72,,0,0,000000000000,2021-07-20T12:42:35,
+73,,0,0,000000000000,2021-07-20T12:42:35,
+74,,0,0,000000000000,2021-07-20T12:42:35,
+75,,0,0,000000000000,2021-07-20T12:42:35,
+76,,0,0,000000000000,2021-07-20T12:42:35,
+77,,0,0,000000000000,2021-07-20T12:42:35,
+78,,0,0,000000000000,2021-07-20T12:42:35,
+79,,0,0,000000000000,2021-07-20T12:42:35,
+80,,0,0,000000000000,2021-07-20T12:42:35,
+81,,0,0,000000000000,2021-07-20T12:42:35,
+82,,0,0,000000000000,2021-07-20T12:42:35,
+83,,0,0,000000000000,2021-07-20T12:42:35,
+84,,0,0,000000000000,2021-07-20T12:42:35,
+85,,0,0,000000000000,2021-07-20T12:42:35,
+86,,0,0,000000000000,2021-07-20T12:42:35,
+87,,0,0,000000000000,2021-07-20T12:42:35,
+88,,0,0,000000000000,2021-07-20T12:42:35,
+89,,0,0,000000000000,2021-07-20T12:42:35,
+90,,0,0,000000000000,2021-07-20T12:42:35,
+91,,0,0,000000000000,2021-07-20T12:42:35,
+92,,0,0,000000000000,2021-07-20T12:42:35,
+93,,0,0,000000000000,2021-07-20T12:42:35,
+94,,0,0,000000000000,2021-07-20T12:42:35,
+95,,0,0,000000000000,2021-07-20T12:42:35,
+96,,0,0,000000000000,2021-07-20T12:42:35,
+97,,0,0,000000000000,2021-07-20T12:42:35,
+98,,0,0,000000000000,2021-07-20T12:42:35,
+99,,0,0,000000000000,2021-07-20T12:42:35,
+100,,0,0,000000000000,2021-07-20T12:42:35,