Repository: privacyidea/privacyidea

Issue:
The content policies are not consistent with the documentation
The PIN policy content is not consistent with the documentation.
The "-" behaves in a different way, than it is documented.

Check, to either fix the behaviour or adapt the documenation.

in addition we might add a functonality, that allows to define a list of allowed chars in the PIN, like
``[0123456789abcsef]`` allowing hex characters ;-)


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/policy.py b/privacyidea/lib/policy.py
--- a/privacyidea/lib/policy.py
+++ b/privacyidea/lib/policy.py
@@ -1567,7 +1567,8 @@ def get_static_policy_definitions(scope=None):
                                     "desc": _("Specifiy the required "
                                               "contents of the OTP PIN. "
                                               "(c)haracters, (n)umeric, "
-                                              "(s)pecial, (o)thers. [+/-]!"),
+                                              "(s)pecial. Use modifiers +/- or a list "
+                                              "of allowed characters [1234567890]"),
                                     'group': GROUP.PIN},
             ACTION.OTPPINSETRANDOM: {
                 'type': 'int',
@@ -1788,7 +1789,8 @@ def get_static_policy_definitions(scope=None):
                                     "desc": _("Specifiy the required "
                                               "contents of the OTP PIN. "
                                               "(c)haracters, (n)umeric, "
-                                              "(s)pecial, (o)thers. [+/-]!"),
+                                              "(s)pecial. Use modifiers +/- or a list "
+                                              "of allowed characters [1234567890]"),
                                     'group': GROUP.PIN},
 
             ACTION.AUDIT: {
diff --git a/privacyidea/lib/utils/__init__.py b/privacyidea/lib/utils/__init__.py
--- a/privacyidea/lib/utils/__init__.py
+++ b/privacyidea/lib/utils/__init__.py
@@ -1115,22 +1115,26 @@ def check_pin_policy(pin, policy):
     "cn" means, that the PIN should contain a character and a number.
     "+cn" means, that the PIN should contain elements from the group of characters and numbers
     "-ns" means, that the PIN must not contain numbers or special characters
+    "[12345]" means, that the PIN may only consist of the characters 1,2,3,4 and 5.
 
     :param pin: The PIN to check
     :param policy: The policy that describes the allowed contents of the PIN.
     :return: Tuple of True or False and a description
     """
-    chars = {"c": "[a-zA-Z]",
-             "n": "[0-9]",
-             "s": "[.:,;_<>+*!/()=?$§%&#~\^-]"}
-    exclusion = False
-    grouping = False
+    chars = {"c": r"[a-zA-Z]",
+             "n": r"[0-9]",
+             "s": r"[\[\].:,;_<>+*!/()=?$§%&#~^-]"}
     ret = True
     comment = []
 
     if not policy:
         return False, "No policy given."
 
+    if policy[0] in ["+", "-"] or policy[0] is not "[":
+        for char in policy[1:]:
+            if char not in chars.keys():
+                raise PolicyError("Unknown character specifier in PIN policy.")
+
     if policy[0] == "+":
         # grouping
         necessary = []
@@ -1151,6 +1155,13 @@ def check_pin_policy(pin, policy):
             ret = False
             comment.append("Not allowed character in PIN!")
 
+    elif policy[0] == "[" and policy[-1] == "]":
+        # only allowed characters
+        allowed_chars = policy[1:-1]
+        for ch in pin:
+            if ch not in allowed_chars:
+                ret = False
+                comment.append("Not allowed character in PIN!")
     else:
         for c in chars:
             if c in policy and not re.search(chars[c], pin):


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
@@ -511,21 +511,33 @@ def test_19_truncate_comma_list(self):
         self.assertEqual(r, "12,+")
 
     def test_20_pin_policy(self):
+        # Unspecified character specifier
+        self.assertRaises(PolicyError, check_pin_policy, "1234", "+o")
+
         r, c = check_pin_policy("1234", "n")
         self.assertTrue(r)
 
+        r, c = check_pin_policy("[[[", "n")
+        self.assertFalse(r)
+
+        r, c = check_pin_policy("[[[", "c")
+        self.assertFalse(r)
+
+        r, c = check_pin_policy("[[[", "s")
+        self.assertTrue(r)
+
         r, c = check_pin_policy("abc", "nc")
         self.assertFalse(r)
         self.assertEqual("Missing character in PIN: [0-9]", c)
 
         r, c = check_pin_policy("123", "nc")
         self.assertFalse(r)
-        self.assertEqual("Missing character in PIN: [a-zA-Z]", c)
+        self.assertEqual(r"Missing character in PIN: [a-zA-Z]", c)
 
         r, c = check_pin_policy("123", "ncs")
         self.assertFalse(r)
-        self.assertTrue("Missing character in PIN: [a-zA-Z]" in c, c)
-        self.assertTrue("Missing character in PIN: [.:,;_<>+*!/()=?$§%&#~\^-]" in c, c)
+        self.assertTrue(r"Missing character in PIN: [a-zA-Z]" in c, c)
+        self.assertTrue(r"Missing character in PIN: [\[\].:,;_<>+*!/()=?$§%&#~^-]" in c, c)
 
         r, c = check_pin_policy("1234", "")
         self.assertFalse(r)
@@ -540,6 +552,10 @@ def test_20_pin_policy(self):
 
         r, c = check_pin_policy("xxxx", "+cn")
         self.assertTrue(r)
+        self.assertTrue(check_pin_policy("test1234", "+cn")[0])
+        self.assertTrue(check_pin_policy("test12$$", "+cn")[0])
+        self.assertTrue(check_pin_policy("test12", "+cn")[0])
+        self.assertTrue(check_pin_policy("1234", "+cn")[0])
 
         r, c = check_pin_policy("@@@@", "+cn")
         self.assertFalse(r)
@@ -549,14 +565,36 @@ def test_20_pin_policy(self):
         # No special character
         r, c = check_pin_policy("1234", "-s")
         self.assertTrue(r)
+        r, c = check_pin_policy("1234aaaa", "-s")
+        self.assertTrue(r)
+        r, c = check_pin_policy("1234aaaa//", "-s")
+        self.assertFalse(r)
 
-        r, c = check_pin_policy("1234", "-sn")
+        # A pin that falsely contains a number
+        r, c = check_pin_policy("1234aaa", "-sn")
         self.assertFalse(r)
         self.assertEqual(c, "Not allowed character in PIN!")
+        r, c = check_pin_policy("///aaa", "-sn")
+        self.assertFalse(r)
+        # A pin without a number and without a special
+        r, c = check_pin_policy("xxxx", "-sn")
+        self.assertTrue(r)
 
         r, c = check_pin_policy("1234@@@@", "-c")
         self.assertTrue(r)
 
+        # A pin with only digits allowed
+        r, c = check_pin_policy("1234", "-cs")
+        self.assertTrue(r)
+        r, c = check_pin_policy("a1234", "-cs")
+        self.assertFalse(r)
+
+        # A pin with only a specified list of chars
+        r, c = check_pin_policy("1234111", "[1234]")
+        self.assertTrue(r)
+        r, c = check_pin_policy("12345", "[1234]")
+        self.assertFalse(r)
+
     def test_21_get_module_class(self):
         r = get_module_class("privacyidea.lib.auditmodules.sqlaudit", "Audit", "log")
         from privacyidea.lib.auditmodules.sqlaudit import Audit