Repository: privacyidea/privacyidea

Issue:
Problems with webhook handler module
### Top-level intent

User the Webhook handler module

### Steps to reproduce

1. create a new event based on auth Eg.:

{   'event': [   {   'action': 'post_webhook',
                     'active': True,
                     'condition': '',
                     'conditions': {},
                     'event': ['auth'],
                     'handlermodule': 'WebHook',
                     'id': 12,
                     'name': 'webhook_post_auth',
                     'options': {   'URL': 'http://localhost:9000/hooks/webhook',
                                    'content_type': 'json',
                                    'data': 'fake data',
                                    'replace': 'False'},
                     'ordering': 0,
                     'position': 'post'}]}

### Expected outcome

Some data should arrive to http://localhost:9000/hooks/webhook

### Actual outcome

When a user tries to authenticate, authentication fails.
No data is sent, I've used a network sniffer to be sure.

### Configuration

* **privacyIDEA version**: 3.8.1
* **Installation method**: pip3
* **Python version**: 3.9.2
* **Operating system**: Linux (Debian 11.7)
* **Webserver**: apache 2.4.56-1~deb11u2
* **Token database**: MySQL

### Log file

~~~~
[2023-07-06 10:12:18,249][413205][140129874433792][ERROR][privacyidea.app:1891] Exception on /auth [POST]
Traceback (most recent call last):
  File "/opt/privacyidea/lib/python3.9/site-packages/flask/app.py", line 2447, in wsgi_app
    response = self.full_dispatch_request()
  File "/opt/privacyidea/lib/python3.9/site-packages/flask/app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/opt/privacyidea/lib/python3.9/site-packages/flask/app.py", line 1821, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/opt/privacyidea/lib/python3.9/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/opt/privacyidea/lib/python3.9/site-packages/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/opt/privacyidea/lib/python3.9/site-packages/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/prepolicy.py", line 158, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/prepolicy.py", line 158, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/prepolicy.py", line 158, in policy_wrapper
    return wrapped_function(*args, **kwds)
  [Previous line repeated 2 more times]
  File "/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/postpolicy.py", line 112, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/postpolicy.py", line 112, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/postpolicy.py", line 112, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  [Previous line repeated 3 more times]
  File "/opt/privacyidea/lib/python3.9/site-packages/privacyidea/lib/event.py", line 134, in event_wrapper
    event_handler.do(e_handler_def.get("action"),
  File "/opt/privacyidea/lib/python3.9/site-packages/privacyidea/lib/eventhandler/webhookeventhandler.py", line 133, in do
    user = User(login=g.logged_in_user.get('username'),
  File "/opt/privacyidea/lib/python3.9/site-packages/werkzeug/local.py", line 347, in __getattr__
    return getattr(self._get_current_object(), name)
AttributeError: '_AppCtxGlobals' object has no attribute 'logged_in_user'
~~~~


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/eventhandler/webhookeventhandler.py b/privacyidea/lib/eventhandler/webhookeventhandler.py
--- a/privacyidea/lib/eventhandler/webhookeventhandler.py
+++ b/privacyidea/lib/eventhandler/webhookeventhandler.py
@@ -31,6 +31,7 @@
 from requests.exceptions import HTTPError, Timeout, ConnectionError, RequestException
 from privacyidea.lib.user import User
 from privacyidea.lib.utils import replace_function_event_handler
+from privacyidea.lib.error import UserError
 
 
 log = logging.getLogger(__name__)
@@ -130,8 +131,13 @@ def do(self, action, options=None):
         if request is not None:
             token_serial = request.all_data.get('serial')
             tokenowner = self._get_tokenowner(request)
-        user = User(login=g.logged_in_user.get('username'),
-                    realm=g.logged_in_user.get('realm'))
+
+        try:
+            user = User(login=g.logged_in_user.get('username'),
+                        realm=g.logged_in_user.get('realm'))
+        except (UserError, AttributeError) as e:  # pragma: no cover
+            log.info('Could not determine user: {0!s}'.format(e))
+            user = None
 
         if replace:
             webhook_text = replace_function_event_handler(webhook_text, token_serial=token_serial,


Test patch:
diff --git a/tests/test_lib_events.py b/tests/test_lib_events.py
--- a/tests/test_lib_events.py
+++ b/tests/test_lib_events.py
@@ -6,6 +6,7 @@
 lib/eventhandler/usernotification.py (one event handler module)
 lib/event.py (the decorator)
 """
+import requests.exceptions
 import responses
 import os
 import mock
@@ -1059,12 +1060,12 @@ def test_02_forward_admin_request(self):
         from privacyidea.lib.eventhandler.federationhandler import ACTION_TYPE
         from privacyidea.lib.privacyideaserver import add_privacyideaserver
         responses.add(responses.POST, "https://remote/token/init",
-                      body="""{"jsonrpc": "2.0", 
-                               "detail": {"googleurl": 
-                                              {"value": "otpauth://totp/TOTP0019C11A?secret=5IUZZICQQI7CFA6VZA4HO6L52RA4ZIVC&period=30&digits=6&issuer=privacyIDEA", 
-                                               "description": "URL for google Authenticator", 
+                      body="""{"jsonrpc": "2.0",
+                               "detail": {"googleurl":
+                                              {"value": "otpauth://totp/TOTP0019C11A?secret=5IUZZICQQI7CFA6VZA4HO6L52RA4ZIVC&period=30&digits=6&issuer=privacyIDEA",
+                                               "description": "URL for google Authenticator",
                                                "img": "data:image/png;base64,YII="},
-                               "threadid": 140161650956032}, 
+                               "threadid": 140161650956032},
                                "versionnumber": "2.20.1",
                                "version": "privacyIDEA 2.20.1",
                                "result": {"status": true,
@@ -2426,7 +2427,6 @@ def test_13_tokengroup(self):
 
         remove_token("SPASS01")
 
-
     def test_14_attach_token(self):
         # create token
         init_token({"serial": "offHOTP", "genkey": 1})
@@ -2475,7 +2475,6 @@ def test_14_attach_token(self):
         self.assertEqual(token_obj.get("machine_id"), "any machine")
 
 
-
 class CustomUserAttributesTestCase(MyTestCase):
 
     def test_01_event_set_attributes_logged_in_user(self):
@@ -2636,15 +2635,16 @@ def test_05_overwrite_existing_attribute(self):
 
 class WebhookTestCase(MyTestCase):
 
+    def setUp(self):
+        super(WebhookTestCase, self).setUp()
+        self.setUp_user_realms()
+
     @patch('requests.post')
     def test_01_send_webhook(self, mock_post):
         with mock.patch("logging.Logger.info") as mock_log:
             mock_post.return_value.status_code = 200
             mock_post.return_value.json.return_value = 'response'
-            # Setup realm and user
-            self.setUp_user_realms()
 
-            user = User("hans", self.realm1)
             g = FakeFlaskG()
             g.logged_in_user = {'username': 'hans',
                                 'realm': self.realm1}
@@ -2694,12 +2694,12 @@ def test_02_actions_and_positions(self):
             "URL": {
                 "type": "str",
                 "required": True,
-                "description": ("The URL the WebHook is posted to")
+                "description": "The URL the WebHook is posted to"
             },
             "content_type": {
                 "type": "str",
                 "required": True,
-                "description": ("The encoding that is sent to the WebHook, for example json"),
+                "description": "The encoding that is sent to the WebHook, for example json",
                 "value": [
                     CONTENT_TYPE.JSON,
                     CONTENT_TYPE.URLENCODED]
@@ -2707,21 +2707,17 @@ def test_02_actions_and_positions(self):
             "replace": {
                 "type": "bool",
                 "required": True,
-                "description": ("You can replace placeholder like {logged_in_user}")
+                "description": "You can replace placeholder like {logged_in_user}"
             },
             "data": {
                 "type": "str",
                 "required": True,
-                "description": ('The data posted in the WebHook')
+                "description": 'The data posted in the WebHook'
             }
         }})
 
     def test_03_wrong_action_type(self):
         with mock.patch("logging.Logger.warning") as mock_log:
-            # Setup realm and user
-            self.setUp_user_realms()
-
-            user = User("hans", self.realm1)
             g = FakeFlaskG()
             g.logged_in_user = {'username': 'hans',
                                 'realm': self.realm1}
@@ -2745,10 +2741,6 @@ def test_03_wrong_action_type(self):
 
     def test_04_wrong_content_type(self):
         with mock.patch("logging.Logger.warning") as mock_log:
-            # Setup realm and user
-            self.setUp_user_realms()
-
-            user = User("hans", self.realm1)
             g = FakeFlaskG()
             g.logged_in_user = {'username': 'hans',
                                 'realm': self.realm1}
@@ -2770,11 +2762,10 @@ def test_04_wrong_content_type(self):
             text = 'Unknown content type value: False_Type'
             mock_log.assert_any_call(text)
 
-    def test_05_wrong_url(self):
-        # Setup realm and user
-        self.setUp_user_realms()
+    @patch('requests.post')
+    def test_05_wrong_url(self, mock_post):
+        mock_post.side_effect = requests.exceptions.ConnectionError()
 
-        user = User("hans", self.realm1)
         g = FakeFlaskG()
         g.logged_in_user = {'username': 'hans',
                             'realm': self.realm1}
@@ -2799,10 +2790,7 @@ def test_06_replace_function(self, mock_post):
         with mock.patch("logging.Logger.info") as mock_log:
             mock_post.return_value.status_code = 200
             mock_post.return_value.json.return_value = 'response'
-            # Setup realm and user
-            self.setUp_user_realms()
 
-            user = User("hans", self.realm1)
             g = FakeFlaskG()
             g.logged_in_user = {'username': 'hans',
                                 'realm': self.realm1}
@@ -2835,9 +2823,6 @@ def test_07_replace_function_error(self, mock_post):
                 mock_post.return_value.status_code = 200
                 mock_post.return_value.json.return_value = 'response'
 
-                # Setup realm and user
-                self.setUp_user_realms()
-
                 init_token({"serial": "SPASS01", "type": "spass"},
                            User("cornelius", self.realm1))
                 g = FakeFlaskG()
@@ -2863,16 +2848,16 @@ def test_07_replace_function_error(self, mock_post):
                                            "replace":
                                                True,
                                            "data":
-                                               '{{token_serial} {token_owner} {user_realm}}'
+                                               '{token_serial} {token_owner} {unknown_tag}'
                                            }
                            }
                            }
                 res = t_handler.do("post_webhook", options=options)
                 self.assertTrue(res)
-                mock_log.assert_any_call("Unable to replace placeholder: (Single '}' encountered in format string)!"
+                mock_log.assert_any_call("Unable to replace placeholder: ('unknown_tag')!"
                                          " Please check the webhooks data option.")
                 text = 'A webhook is send to {0!r} with the text: {1!r}'.format(
-                    'http://test.com', '{{token_serial} {token_owner} {user_realm}}')
+                    'http://test.com', '{token_serial} {token_owner} {unknown_tag}')
                 mock_info.assert_any_call(text)
                 mock_info.assert_called_with(200)
 
@@ -2883,9 +2868,6 @@ def test_08_replace_function_typo(self, mock_post):
                 mock_post.return_value.status_code = 200
                 mock_post.return_value.json.return_value = 'response'
 
-                # Setup realm and user
-                self.setUp_user_realms()
-
                 init_token({"serial": "SPASS01", "type": "spass"},
                            User("cornelius", self.realm1))
                 g = FakeFlaskG()