Repository: privacyidea/privacyidea

Issue:
Error on new failcounter_clear_timeout: parser must be a string 
<!--
Thank you for filing a bug within privacyIDEA.
Please read the following blog post.
https://www.privacyidea.org/getting-help/

The more information you provide, the easier it is to identify 
the problem!

Issues are not meant for asking questions!
If you simply wonder how things work and need some explanation 
please visit the community forum:

https://community.privacyidea.org

If you do not like the NO-WARRANTY of this software and need
reliable, professional help, please consider getting either
- consultancy 
  (https://netknights.it/en/leistungen/one-time-services/)
- or the privacyIDEA Enterprise Edition 
  (https://netknights.it/en/leistungen/service-level-agreements/)

If you still think, you should open an issue, please do so:
-->

On an upgraded installation, when `failcounter_clear_timeout` is set, an exception can occur if a token has a 0 failcount.

The exception:

```
ERROR in app: Exception on /validate/check [POST]
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/postpolicy.py", line 102, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/postpolicy.py", line 102, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/postpolicy.py", line 102, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/postpolicy.py", line 102, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/postpolicy.py", line 102, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/postpolicy.py", line 102, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/postpolicy.py", line 102, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/postpolicy.py", line 102, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/prepolicy.py", line 117, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/prepolicy.py", line 117, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/prepolicy.py", line 117, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/decorators.py", line 91, in check_user_or_serial_in_request_wrapper
    f_result = func(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/subscriptions.py", line 316, in check_subscription_wrapper
    f_result = func(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/lib/prepolicy.py", line 117, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/event.py", line 60, in event_wrapper
    f_result = func(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/api/validate.py", line 299, in check
    result, details = check_user_pass(user, password, options=options)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 89, in policy_wrapper
    return self.decorator_function(wrapped_function, *args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 196, in auth_cache
    return wrapped_function(user_object, passw, options)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 89, in policy_wrapper
    return self.decorator_function(wrapped_function, *args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 273, in auth_user_does_not_exist
    return wrapped_function(user_object, passw, options)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 89, in policy_wrapper
    return self.decorator_function(wrapped_function, *args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 235, in auth_user_has_no_token
    return wrapped_function(user_object, passw, options)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 89, in policy_wrapper
    return self.decorator_function(wrapped_function, *args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 352, in auth_user_timelimit
    res, reply_dict = wrapped_function(user_object, passw, options)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 89, in policy_wrapper
    return self.decorator_function(wrapped_function, *args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 440, in auth_lastauth
    res, reply_dict = wrapped_function(user_or_serial, passw, options)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 89, in policy_wrapper
    return self.decorator_function(wrapped_function, *args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/policydecorators.py", line 329, in auth_user_passthru
    return wrapped_function(user_object, passw, options)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/log.py", line 154, in log_wrapper
    return func(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/token.py", line 1896, in check_user_pass
    options=options)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/log.py", line 154, in log_wrapper
    return func(*args, **kwds)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/token.py", line 2016, in check_token_list
    if token_obj.check_all(message_list):
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/tokenclass.py", line 1089, in check_all
    elif not self.check_failcount():
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/tokenclass.py", line 1017, in check_failcount
    FAILCOUNTER_EXCEEDED), return_date=True)
  File "/usr/local/lib/python2.7/dist-packages/privacyidea/lib/utils.py", line 731, in parse_legacy_time
    d = parse_date_string(ts)
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 1182, in parse
    return DEFAULTPARSER.parse(timestr, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 556, in parse
    res, skipped_tokens = self._parse(timestr, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 675, in _parse
    l = _timelex.split(timestr)         # Splits the timestr into tokens
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 192, in split
    return list(cls(s))
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 61, in __init__
    '{itype}'.format(itype=instream.__class__.__name__))
TypeError: Parser must be a string or character stream, not NoneType
```

### Configuration

* privacyIDEA Version: 2.20

* Installation method: pip

### Mitigation

It appears that this line deletes `failcounter_exceeded`:

https://github.com/privacyidea/privacyidea/blob/3411738135981827b3cebefee6840452c2db2c9c/privacyidea/lib/tokenclass.py#L613

Which causes this to fail:

https://github.com/privacyidea/privacyidea/blob/3411738135981827b3cebefee6840452c2db2c9c/privacyidea/lib/tokenclass.py#L1016-L1017

Because that method will crash if you pass it `None`.

A few mitigations:

1. null-check `self.get_tokeninfo(FAILCOUNTER_EXCEEDED)`
2. Only check if the counter is `> 0` before even looking this up (nothing to reset)

Happy to send a PR if you like.



Discussion:

Resolving patch:
diff --git a/privacyidea/lib/tokenclass.py b/privacyidea/lib/tokenclass.py
--- a/privacyidea/lib/tokenclass.py
+++ b/privacyidea/lib/tokenclass.py
@@ -1009,12 +1009,13 @@ def check_failcount(self):
             log.warning("Misconfiguration. Error retrieving "
                         "failcounter_clear_timeout: "
                         "{0!s}".format(exx))
-        if timeout:
+        if timeout and self.token.failcount > 0:
             now = datetime.datetime.now(tzlocal())
-            failcounter_exceeded = parse_legacy_time(self.get_tokeninfo(
-                FAILCOUNTER_EXCEEDED), return_date=True)
-            if now > failcounter_exceeded + datetime.timedelta(minutes=timeout):
-                self.reset()
+            lastfail = self.get_tokeninfo(FAILCOUNTER_EXCEEDED)
+            if lastfail is not None:
+                failcounter_exceeded = parse_legacy_time(lastfail, return_date=True)
+                if now > failcounter_exceeded + datetime.timedelta(minutes=timeout):
+                    self.reset()
 
         return self.token.failcount < self.token.maxfail
 


Test patch: