Repository: privacyidea/privacyidea

Issue:
Some time deltas do not support specifying seconds
Right now, we have two functions to parse time deltas:

``parse_timedelta`` only parses positive timedeltas, but does not support seconds:
https://github.com/privacyidea/privacyidea/blob/834ff2f77c37863244a94a58ae7b159d70bbc809/privacyidea/lib/utils.py#L385

``parse_time_delta`` supports negative timedeltas and requires "+" and "-" prefixes, and also supports seconds.
https://github.com/privacyidea/privacyidea/blob/834ff2f77c37863244a94a58ae7b159d70bbc809/privacyidea/lib/utils.py#L782

As a result, the auth cache does not support specifying time deltas in seconds, because it uses ``parse_timedelta``. Other features may behave similarly.

We should probably consolidate these methods somehow? But we need to keep in mind that for some cases, specifying negative time deltas might not make sense.


Discussion:
I would not call this a bug. Everything works as "designed". It is simply a bad design and we have a redundant function...

I will push a PR in a few seconds
..ah no. I will run my local tests... :-)

Resolving patch:
diff --git a/privacyidea/lib/utils.py b/privacyidea/lib/utils.py
--- a/privacyidea/lib/utils.py
+++ b/privacyidea/lib/utils.py
@@ -382,33 +382,6 @@ def get_data_from_params(params, exclude_params, config_description, module,
     return data, types, desc
 
 
-def parse_timedelta(delta):
-    """
-    This parses a string that contains a time delta for the last_auth policy
-    in the format like
-    1m (minute), 1h, 1d, 1y.
-
-    :param delta: The time delta
-    :type delta: basestring
-    :return: timedelta
-    """
-    delta = delta.strip().replace(" ", "")
-    time_specifier = delta[-1].lower()
-    if time_specifier not in ["m", "h", "d", "y"]:
-        raise Exception("Invalid time specifier")
-    time = int(delta[:-1])
-    if time_specifier == "h":
-        td = timedelta(hours=time)
-    elif time_specifier == "d":
-        td = timedelta(days=time)
-    elif time_specifier == "y":
-        td = timedelta(days=time*365)
-    else:
-        td = timedelta(minutes=time)
-
-    return td
-
-
 def parse_timelimit(limit):
     """
     This parses a string that contains a timelimit in the format
@@ -779,10 +752,10 @@ def parse_legacy_time(ts, return_date=False):
         return d.strftime(DATE_FORMAT)
 
 
-def parse_time_delta(s):
+def parse_timedelta(s):
     """
     parses a string like +5d or -30m and returns a timedelta.
-    Allowed identifiers are s, m, h, d.
+    Allowed identifiers are s, m, h, d, y.
     
     :param s: a string like +30m or -5d
     :return: timedelta 
@@ -791,7 +764,7 @@ def parse_time_delta(s):
     minutes = 0
     hours = 0
     days = 0
-    m = re.match("([+-])(\d+)([smhd])$", s)
+    m = re.match("\s*([+-]?)\s*(\d+)\s*([smhdy])\s*$", s)
     if not m:
         log.warning("Unsupported timedelta: {0!r}".format(s))
         raise Exception("Unsupported timedelta")
@@ -806,6 +779,8 @@ def parse_time_delta(s):
         hours = count
     elif m.group(3) == "d":
         days = count
+    elif m.group(3) == "y":
+        days = 365 * count
 
     td = timedelta(seconds=seconds, minutes=minutes, hours=hours, days=days)
     return td
@@ -834,7 +809,7 @@ def parse_time_offset_from_now(s):
         s2 = m.group(2)
         s3 = m.group(3)
         s = s1 + s3
-        td = parse_time_delta(s2)
+        td = parse_timedelta(s2)
 
     return s, td
 


Test patch:
diff --git a/tests/test_lib_utils.py b/tests/test_lib_utils.py
--- a/tests/test_lib_utils.py
+++ b/tests/test_lib_utils.py
@@ -4,14 +4,14 @@
 """
 from .base import MyTestCase
 
-from privacyidea.lib.utils import (parse_timelimit, parse_timedelta,
+from privacyidea.lib.utils import (parse_timelimit,
                                    check_time_in_range, parse_proxy,
                                    check_proxy, reduce_realms, is_true,
                                    parse_date, compare_condition,
                                    get_data_from_params, parse_legacy_time,
                                    int_to_hex, compare_value_value,
                                    parse_time_offset_from_now,
-                                   parse_time_delta, to_unicode,
+                                   parse_timedelta, to_unicode,
                                    hash_password, PasswordHash, check_ssha,
                                    check_sha, otrs_sha256, parse_int)
 from datetime import timedelta, datetime
@@ -329,19 +329,21 @@ def test_12_compare_value_value(self):
             ">", datetime.now(tzlocal()).strftime(DATE_FORMAT)))
 
     def test_13_parse_time_offset_from_now(self):
-        td = parse_time_delta("+5s")
+        td = parse_timedelta("+5s")
         self.assertEqual(td, timedelta(seconds=5))
-        td = parse_time_delta("-12m")
+        td = parse_timedelta("-12m")
         self.assertEqual(td, timedelta(minutes=-12))
-        td = parse_time_delta("+123h")
+        td = parse_timedelta("+123h")
         self.assertEqual(td, timedelta(hours=123))
-        td = parse_time_delta("+2d")
+        td = parse_timedelta("+2d")
         self.assertEqual(td, timedelta(days=2))
 
-        # Does not start with plus or minus
-        self.assertRaises(Exception, parse_time_delta, "12d")
+        # It is allowed to start without a +/- which would mean a +
+        td = parse_timedelta("12d")
+        self.assertEqual(td, timedelta(days=12))
+
         # Does not contains numbers
-        self.assertRaises(Exception, parse_time_delta, "+twod")
+        self.assertRaises(Exception, parse_timedelta, "+twod")
 
         s, td = parse_time_offset_from_now("Hello {now}+5d with 5 days.")
         self.assertEqual(s, "Hello {now} with 5 days.")