Repository: privacyidea/privacyidea

Issue:
check policies for valid keys
At the moment there are some policies which allow the usage of keys like `{serial}` or `{realm}`.

Typos in policy keys happen quickly.

When the key is used by the code, privacyIDEA throws an according exception, like here for an /token/init with a "tokenlabel="{real}".

```
[2021-08-03 17:40:48,049][6113][140010240476928][ERROR][privacyidea.lib.tokens.hotptoken:292] Traceback (most recent call last):
  File "/opt/privacyidea/lib/python3.6/site-packages/privacyidea/lib/tokens/hotptoken.py", line 268, in get_init_detail
    extra_data=extra_data)
  File "/opt/privacyidea/lib/python3.6/site-packages/privacyidea/lib/log.py", line 155, in log_wrapper
    return func(*args, **kwds)
  File "/opt/privacyidea/lib/python3.6/site-packages/privacyidea/lib/apps.py", line 140, in create_google_authenticator_url
    surname=user_obj.info.get("surname", ""))
KeyError: 'real'

[2021-08-03 17:40:48,049][6113][140010240476928][ERROR][privacyidea.lib.tokens.hotptoken:293] failed to set oath or google url: KeyError('real',)
```


To the user this does not provide much information about the incorrect policy. In this case, the frontend user only notices, that the QR-Code of the HOTP token is not displayed during rollout.

It would be nice to catch such errors or to check policies for the supported key words.

check policies for valid keys
At the moment there are some policies which allow the usage of keys like `{serial}` or `{realm}`.

Typos in policy keys happen quickly.

When the key is used by the code, privacyIDEA throws an according exception, like here for an /token/init with a "tokenlabel="{real}".

```
[2021-08-03 17:40:48,049][6113][140010240476928][ERROR][privacyidea.lib.tokens.hotptoken:292] Traceback (most recent call last):
  File "/opt/privacyidea/lib/python3.6/site-packages/privacyidea/lib/tokens/hotptoken.py", line 268, in get_init_detail
    extra_data=extra_data)
  File "/opt/privacyidea/lib/python3.6/site-packages/privacyidea/lib/log.py", line 155, in log_wrapper
    return func(*args, **kwds)
  File "/opt/privacyidea/lib/python3.6/site-packages/privacyidea/lib/apps.py", line 140, in create_google_authenticator_url
    surname=user_obj.info.get("surname", ""))
KeyError: 'real'

[2021-08-03 17:40:48,049][6113][140010240476928][ERROR][privacyidea.lib.tokens.hotptoken:293] failed to set oath or google url: KeyError('real',)
```


To the user this does not provide much information about the incorrect policy. In this case, the frontend user only notices, that the QR-Code of the HOTP token is not displayed during rollout.

It would be nice to catch such errors or to check policies for the supported key words.



Discussion:
Since upon error, it is hard to track down the policy with wrong values, it might be a good idea to check policies with keywords for their correctness at set policy.
@jona-samuel Please check, if this is still relevant and if we should take action or if we should simply close this.
This is still relevant. We can catch the key error and give the user a more detailed error
Catch the KeyError and raise an EnrollmentError or PolicyError like

"Unknown Tag (<tag name>) in your policy definition <name of policy>".

See https://github.com/privacyidea/privacyidea/blob/fe3c5e299a7e68761edc3f6636763f7eb5c14d2f/privacyidea/lib/apps.py#L138
Since upon error, it is hard to track down the policy with wrong values, it might be a good idea to check policies with keywords for their correctness at set policy.
@jona-samuel Please check, if this is still relevant and if we should take action or if we should simply close this.
This is still relevant. We can catch the key error and give the user a more detailed error
Catch the KeyError and raise an EnrollmentError or PolicyError like

"Unknown Tag (<tag name>) in your policy definition <name of policy>".

See https://github.com/privacyidea/privacyidea/blob/fe3c5e299a7e68761edc3f6636763f7eb5c14d2f/privacyidea/lib/apps.py#L138

Resolving patch:
diff --git a/privacyidea/lib/tokens/hotptoken.py b/privacyidea/lib/tokens/hotptoken.py
--- a/privacyidea/lib/tokens/hotptoken.py
+++ b/privacyidea/lib/tokens/hotptoken.py
@@ -298,8 +298,12 @@ def get_init_detail(self, params=None, user=None):
                                                   "value": oath_url,
                                                   "img": create_img(oath_url)
                                                   }
+                except KeyError as ex:
+                    log.debug("{0!s}".format((traceback.format_exc())))
+                    log.error('Unknown Tag {0!s} in one of your policy definition'
+                              .format(ex))
                 except Exception as ex:  # pragma: no cover
-                    log.error("{0!s}".format((traceback.format_exc())))
+                    log.debug("{0!s}".format((traceback.format_exc())))
                     log.error('failed to set oath or google url: {0!r}'.format(ex))
 
         return response_detail


Test patch:
diff --git a/tests/test_lib_tokens_hotp.py b/tests/test_lib_tokens_hotp.py
--- a/tests/test_lib_tokens_hotp.py
+++ b/tests/test_lib_tokens_hotp.py
@@ -16,14 +16,15 @@
 from privacyidea.lib.utils import b32encode_and_unicode
 from privacyidea.lib.tokens.hotptoken import HotpTokenClass
 from privacyidea.models import (Token,
-                                 Config,
-                                 Challenge)
+                                Config,
+                                Challenge)
 from privacyidea.lib.config import (set_privacyidea_config, set_prepend_pin)
 from privacyidea.lib.policy import (PolicyClass, SCOPE, set_policy,
                                     delete_policy)
 import binascii
 import datetime
 import hashlib
+import mock
 from dateutil.tz import tzlocal
 
 from passlib.crypto.digest import pbkdf2_hmac
@@ -47,8 +48,8 @@ class HOTPTokenTestCase(MyTestCase):
 
     def test_00_create_user_realm(self):
         rid = save_resolver({"resolver": self.resolvername1,
-                               "type": "passwdresolver",
-                               "fileName": PWFILE})
+                             "type": "passwdresolver",
+                             "fileName": PWFILE})
         self.assertTrue(rid > 0, rid)
 
         (added, failed) = set_realm(self.realm1,
@@ -439,15 +440,15 @@ def test_18_challenges(self):
         transaction_id = "123456789"
         resp = token.is_challenge_response(User(login="cornelius",
                                                 realm=self.realm1),
-                                            "test123456")
+                                           "test123456")
         self.assertFalse(resp, resp)
 
         C = Challenge(self.serial1, transaction_id=transaction_id, challenge="Who are you?")
         C.save()
         resp = token.is_challenge_response(User(login="cornelius",
                                                 realm=self.realm1),
-                                            "test123456",
-                                            options={"transaction_id": transaction_id})
+                                           "test123456",
+                                           options={"transaction_id": transaction_id})
         self.assertTrue(resp, resp)
         # test if challenge is valid
         self.assertTrue(C.is_valid())
@@ -696,8 +697,8 @@ def test_27_get_default_settings(self):
         g = FakeFlaskG()
         g.audit_object = FakeAudit()
         g.logged_in_user = {"user": "hans",
-                          "realm": "default",
-                          "role": "user"}
+                            "realm": "default",
+                            "role": "user"}
         set_policy("pol1", scope=SCOPE.USER, action="hotp_hashlib=sha256,"
                                                     "hotp_otplen=8")
         g.policy_object = PolicyClass()
@@ -763,7 +764,7 @@ def test_29_2step_generation_custom(self):
         server_component = binascii.unhexlify(token.token.get_otpkey().getKey())
         # too short
         self.assertRaises(ParameterError, token.update, {
-            "otpkey": binascii.hexlify(b"="*8)
+            "otpkey": binascii.hexlify(b"=" * 8)
         })
         # generate a 12-byte client component
         client_component = b'abcdefghijkl'
@@ -774,13 +775,13 @@ def test_29_2step_generation_custom(self):
             "2step_serversize": "23",
             "2step_difficulty": "666666",
             "2step_clientsize": "13"
-            })
+        })
         # check the generated secret
         secret = binascii.unhexlify(token.token.get_otpkey().getKey())
         # check the correct lengths
         self.assertEqual(len(server_component), 40)
         self.assertEqual(len(client_component), 12)
-        self.assertEqual(len(secret), 64) # because of SHA-512
+        self.assertEqual(len(secret), 64)  # because of SHA-512
         # check the secret has been generated according to the specification
         expected_secret = pbkdf2_hmac('sha1', binascii.hexlify(server_component),
                                       client_component, 12345, len(secret))
@@ -823,15 +824,25 @@ def test_30_2step_otpkeyformat(self):
             "2step_serversize": "23",
             "2step_difficulty": "666666",
             "2step_clientsize": "13"
-            })
+        })
         # check the generated secret
         secret = binascii.unhexlify(token.token.get_otpkey().getKey())
         # check the correct lengths
-        self.assertEqual(len(server_component), 64) # because of SHA-512
+        self.assertEqual(len(server_component), 64)  # because of SHA-512
         self.assertEqual(len(client_component), 12)
-        self.assertEqual(len(secret), 64) # because of SHA-512
+        self.assertEqual(len(secret), 64)  # because of SHA-512
         # check the secret has been generated according to the specification
         expected_secret = pbkdf2_hmac('sha1', binascii.hexlify(server_component),
                                       client_component, 10000, len(secret))
         self.assertEqual(secret, expected_secret)
         self.assertTrue(token.token.active)
+
+    def test_31_wrong_tag(self):
+        with mock.patch("logging.Logger.error") as mock_log:
+            db_token = Token.query.filter_by(serial=self.serial1).first()
+            token = HotpTokenClass(db_token)
+            token.add_init_details("otpkey", "11223344556677889900")
+            params = {"tokenlabel": "{real}"}
+            _detail = token.get_init_detail(user=User("cornelius",
+                                                      self.realm1), params=params)
+            mock_log.assert_any_call("Unknown Tag 'real' in one of your policy definition")