Repository: privacyidea/privacyidea

Issue:
token-janitor last_auth fails with unknown string format
~~~~shell
% sudo privacyidea-token-janitor find --last_auth 30d

+ Reading tokens from database...
INFO:privacyidea.lib.tokens.vasco:PI_VASCO_LIBRARY option is not set, functionality disabled
++ Creating token object list.
Traceback (most recent call last):nd
  File "/usr/bin/privacyidea-token-janitor", line 590, in <module>
    manager.run()
  File "/opt/privacyidea/lib/python3.8/site-packages/flask_script/__init__.py", line 417, in run
    result = self.handle(argv[0], argv[1:])
  File "/opt/privacyidea/lib/python3.8/site-packages/flask_script/__init__.py", line 386, in handle
    res = handle(*args, **config)
  File "/opt/privacyidea/lib/python3.8/site-packages/flask_script/commands.py", line 216, in __call__
    return self.run(*args, **kwargs)
  File "/usr/bin/privacyidea-token-janitor", line 463, in find
    for tlist in generator:
  File "/usr/bin/privacyidea-token-janitor", line 283, in _get_tokenlist
    if last_auth and token_obj.check_last_auth_newer(last_auth):
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/tokenclass.py", line 1681, in check_last_auth_newer
    last_success_auth = parse_date_string(date_s)
  File "/opt/privacyidea/lib/python3.8/site-packages/dateutil/parser/_parser.py", line 1374, in parse
    return DEFAULTPARSER.parse(timestr, **kwargs)
  File "/opt/privacyidea/lib/python3.8/site-packages/dateutil/parser/_parser.py", line 649, in parse
    raise ParserError("Unknown string format: %s", timestr)
dateutil.parser._parser.ParserError: Unknown string format: hardware
~~~~


Discussion:
Looks as if I messed up my database and set a lot of tokeninfos to "hardware".
Thus, the time can not be parsed correctly. 

I think we can simply put a try-catch around the data parsing of last_auth. Either

* in the token-janitor or
* in the last_auth lib function.


Well, I am not sure, if we should really catch this. Because how should we decide if there is a faulty value in `last_auth`.
Should `check_last_auth_newer` return True or False? Based on this an event would be triggered or a token might be deleted.

If at all, we should catch it in the token-janitor and output an error. But the same would happen in parsing in `compare_before` and `compare_after`.
So I really wonder, if we should stick with the exception, which only would happen if you set crap in the tokeninfo table on the database level.
We should catch the error.
If we e.g. search for tokens "older than date", then a token with a faulty date would not match.
If we search for tokens "newer than date", then a token with a faulty date would also not macht.

Resolving patch:
diff --git a/privacyidea/lib/tokenclass.py b/privacyidea/lib/tokenclass.py
--- a/privacyidea/lib/tokenclass.py
+++ b/privacyidea/lib/tokenclass.py
@@ -95,7 +95,7 @@
                                     generate_otpkey)
 from .policydecorators import libpolicy, auth_otppin, challenge_response_allowed
 from .decorators import check_token_locked
-from dateutil.parser import parse as parse_date_string
+from dateutil.parser import parse as parse_date_string, ParserError
 from dateutil.tz import tzlocal, tzutc
 from privacyidea.lib.utils import (is_true, decode_base32check,
                                    to_unicode, create_img, parse_timedelta,
@@ -1659,7 +1659,10 @@ def check_last_auth_newer(self, last_auth):
         than the specified time delta which is passed as 10h, 7d or 1y.
 
         It returns True, if the last authentication with this token is
-        **newer*** than the specified delta.
+        **newer** than the specified delta or by any chance exactly the same.
+
+        It returns False, if the last authentication is older or
+        if the data in the token can not be parsed.
 
         :param last_auth: 10h, 7d or 1y
         :type last_auth: basestring
@@ -1678,7 +1681,12 @@ def check_last_auth_newer(self, last_auth):
                       "tdelta %s: %s" % (self.token.serial, tdelta,
                                          date_s))
             # parse the string from the database
-            last_success_auth = parse_date_string(date_s)
+            try:
+                last_success_auth = parse_date_string(date_s)
+            except ParserError:
+                log.info("Failed to parse the date in 'last_auth' of token {0!s}.".format(self.token.serial))
+                return False
+
             if not last_success_auth.tzinfo:
                 # the date string has no timezone, default timezone is UTC
                 # We need to set the timezone manually


Test patch:
diff --git a/tests/test_lib_tokenclass.py b/tests/test_lib_tokenclass.py
--- a/tests/test_lib_tokenclass.py
+++ b/tests/test_lib_tokenclass.py
@@ -798,6 +798,13 @@ def test_38_last_auth(self):
         r = token_obj.check_last_auth_newer("10h")
         self.assertFalse(r)
         r = token_obj.check_last_auth_newer("2d")
+        self.assertTrue(r)
+
+        # Test a fault last_auth entry does not computer to True
+        token_obj.add_tokeninfo(ACTION.LASTAUTH, "faulty format")
+        r = token_obj.check_last_auth_newer("10h")
+        self.assertFalse(r)
+
         token_obj.delete_token()
 
     def test_39_generate_sym_key(self):