We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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'
~~~~

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `privacyidea/lib/eventhandler/webhookeventhandler.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 # License:  AGPLv3
4 # (c)
5 #
6 # This code is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
8 # License as published by the Free Software Foundation; either
9 # version 3 of the License, or any later version.
10 #
11 # This code is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU AFFERO GENERAL PUBLIC LICENSE for more details.
15 #
16 # You should have received a copy of the GNU Affero General Public
17 # License along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19 #
20 __doc__ = """This is th event handler module for posting webhooks.
21 You can send an webhook to trigger an event on an other system or use to replace
22 api requests and reduce your traffic this way.
23 
24 """
25 
26 from privacyidea.lib.eventhandler.base import BaseEventHandler
27 from privacyidea.lib import _
28 import json
29 import logging
30 import requests
31 from requests.exceptions import HTTPError, Timeout, ConnectionError, RequestException
32 from privacyidea.lib.user import User
33 from privacyidea.lib.utils import replace_function_event_handler
34 
35 
36 log = logging.getLogger(__name__)
37 TIMEOUT = 10
38 
39 
40 class CONTENT_TYPE(object):
41     """
42     Allowed type off content
43     """
44     JSON = "json"
45     URLENCODED = "urlendcode"
46 
47 
48 class ACTION_TYPE(object):
49     """
50     Allowed actions
51     """
52     POST_WEBHOOK = "post_webhook"
53 
54 
55 class WebHookHandler(BaseEventHandler):
56     """
57     With the WebHook Handler, a webhook can be sent at a particular event
58     """
59 
60     identifier = "WebHookHandler"
61     description = "This eventhandler can post webhooks"
62 
63     @property
64     def allowed_positions(cls):
65         """
66         This returns the allowed positions of the event handler definition.
67 
68         :return: list of allowed positions
69         """
70         return ["post", "pre"]
71 
72     @property
73     def actions(cls):
74         """
75         This method returns a dictionary of allowed actions and possible
76         options in this handler module.
77 
78         :return: dict with actions
79         """
80         # The event handler has just one action. Maybe we can  hide action select for the clarity of the UI
81         actions = {ACTION_TYPE.POST_WEBHOOK: {
82             "URL": {
83                 "type": "str",
84                 "required": True,
85                 "description": _("The URL the WebHook is posted to")
86             },
87             "content_type": {
88                 "type": "str",
89                 "required": True,
90                 "description": _("The encoding that is sent to the WebHook, for example json"),
91                 "value": [
92                     CONTENT_TYPE.JSON,
93                     CONTENT_TYPE.URLENCODED]
94             },
95             "replace": {
96                 "type": "bool",
97                 "required": True,
98                 "description": _("You can replace placeholder like {logged_in_user}")
99             },
100             "data": {
101                 "type": "str",
102                 "required": True,
103                 "description": _("The data posted in the WebHook")
104             }
105         }}
106         return actions
107 
108     def do(self, action, options=None):
109         """
110         This method executes the defined action in the given event.
111 
112         :param action: The action to perform
113         :type action: str
114         :param options: Contains the flask parameters g, URL, data, parameter and the fire and forget
115         :type options: dict
116         :return:
117         """
118         ret = True
119         g = options.get("g")
120         request = options.get("request")
121         handler_def = options.get("handler_def")
122         handler_options = handler_def.get("options")
123         webhook_url = handler_options.get("URL")
124         webhook_text = handler_options.get("data")
125         content_type = handler_options.get("content_type")
126         replace = handler_options.get("replace")
127         token_serial = None
128         tokenowner = None
129 
130         if request is not None:
131             token_serial = request.all_data.get('serial')
132             tokenowner = self._get_tokenowner(request)
133         user = User(login=g.logged_in_user.get('username'),
134                     realm=g.logged_in_user.get('realm'))
135 
136         if replace:
137             webhook_text = replace_function_event_handler(webhook_text, token_serial=token_serial,
138                                                           tokenowner=tokenowner, logged_in_user=user)
139 
140         if action.lower() == ACTION_TYPE.POST_WEBHOOK:
141             try:
142                 if content_type == CONTENT_TYPE.JSON:
143                     """
144                     This is the main function where the webhook is sent.
145                     Documentation of the requests function is found her docs.python-requests.org
146                     """
147                     log.info('A webhook is send to {0!r} with the text: {1!r}'.format(
148                         webhook_url, webhook_text))
149                     resp = requests.post(webhook_url, data=json.dumps(webhook_text),
150                                          headers={'Content-Type': content_type}, timeout=TIMEOUT)
151                     # all answers will be logged in the debug. The HTTP response code will be shown in the audit too
152                     log.info(resp.status_code)
153                     log.debug(resp)
154                 elif content_type == CONTENT_TYPE.URLENCODED:
155                     """
156                     This is the main function where the webhook is sent.
157                     Documentation of the requests function is found her docs.python-requests.org
158                     """
159                     log.info('A webhook is send to {0!r} with the text: {1!r}'.format(
160                         webhook_url, webhook_text))
161                     resp = requests.post(webhook_url, webhook_text,
162                                          headers={'Content-Type': content_type}, timeout=TIMEOUT)
163                     # all answers will be logged in the debug. The HTTP response code will be shown in the audit too
164                     log.info(resp.status_code)
165                     log.debug(resp)
166                 else:
167                     log.warning('Unknown content type value: {0!s}'.format(content_type))
168                     ret = False
169             # error handling
170             except (HTTPError, ConnectionError, RequestException, Timeout) as err:
171                 log.warning(err)
172                 return False
173         else:
174             log.warning('Unknown action value: {0!s}'.format(action))
175             ret = False
176         return ret
177
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
         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,

{"golden_diff": "diff --git a/privacyidea/lib/eventhandler/webhookeventhandler.py b/privacyidea/lib/eventhandler/webhookeventhandler.py\n--- a/privacyidea/lib/eventhandler/webhookeventhandler.py\n+++ b/privacyidea/lib/eventhandler/webhookeventhandler.py\n@@ -31,6 +31,7 @@\n from requests.exceptions import HTTPError, Timeout, ConnectionError, RequestException\n from privacyidea.lib.user import User\n from privacyidea.lib.utils import replace_function_event_handler\n+from privacyidea.lib.error import UserError\n \n \n log = logging.getLogger(__name__)\n@@ -130,8 +131,13 @@\n         if request is not None:\n             token_serial = request.all_data.get('serial')\n             tokenowner = self._get_tokenowner(request)\n-        user = User(login=g.logged_in_user.get('username'),\n-                    realm=g.logged_in_user.get('realm'))\n+\n+        try:\n+            user = User(login=g.logged_in_user.get('username'),\n+                        realm=g.logged_in_user.get('realm'))\n+        except (UserError, AttributeError) as e:  # pragma: no cover\n+            log.info('Could not determine user: {0!s}'.format(e))\n+            user = None\n \n         if replace:\n             webhook_text = replace_function_event_handler(webhook_text, token_serial=token_serial,\n", "issue": "Problems with webhook handler module\n### Top-level intent\r\n\r\nUser the Webhook handler module\r\n\r\n### Steps to reproduce\r\n\r\n1. create a new event based on auth Eg.:\r\n\r\n{   'event': [   {   'action': 'post_webhook',\r\n                     'active': True,\r\n                     'condition': '',\r\n                     'conditions': {},\r\n                     'event': ['auth'],\r\n                     'handlermodule': 'WebHook',\r\n                     'id': 12,\r\n                     'name': 'webhook_post_auth',\r\n                     'options': {   'URL': 'http://localhost:9000/hooks/webhook',\r\n                                    'content_type': 'json',\r\n                                    'data': 'fake data',\r\n                                    'replace': 'False'},\r\n                     'ordering': 0,\r\n                     'position': 'post'}]}\r\n\r\n### Expected outcome\r\n\r\nSome data should arrive to http://localhost:9000/hooks/webhook\r\n\r\n### Actual outcome\r\n\r\nWhen a user tries to authenticate, authentication fails.\r\nNo data is sent, I've used a network sniffer to be sure.\r\n\r\n### Configuration\r\n\r\n* **privacyIDEA version**: 3.8.1\r\n* **Installation method**: pip3\r\n* **Python version**: 3.9.2\r\n* **Operating system**: Linux (Debian 11.7)\r\n* **Webserver**: apache 2.4.56-1~deb11u2\r\n* **Token database**: MySQL\r\n\r\n### Log file\r\n\r\n~~~~\r\n[2023-07-06 10:12:18,249][413205][140129874433792][ERROR][privacyidea.app:1891] Exception on /auth [POST]\r\nTraceback (most recent call last):\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/flask/app.py\", line 2447, in wsgi_app\r\n    response = self.full_dispatch_request()\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/flask/app.py\", line 1952, in full_dispatch_request\r\n    rv = self.handle_user_exception(e)\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/flask/app.py\", line 1821, in handle_user_exception\r\n    reraise(exc_type, exc_value, tb)\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/flask/_compat.py\", line 39, in reraise\r\n    raise value\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/flask/app.py\", line 1950, in full_dispatch_request\r\n    rv = self.dispatch_request()\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/flask/app.py\", line 1936, in dispatch_request\r\n    return self.view_functions[rule.endpoint](**req.view_args)\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/prepolicy.py\", line 158, in policy_wrapper\r\n    return wrapped_function(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/prepolicy.py\", line 158, in policy_wrapper\r\n    return wrapped_function(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/prepolicy.py\", line 158, in policy_wrapper\r\n    return wrapped_function(*args, **kwds)\r\n  [Previous line repeated 2 more times]\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/postpolicy.py\", line 112, in policy_wrapper\r\n    response = wrapped_function(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/postpolicy.py\", line 112, in policy_wrapper\r\n    response = wrapped_function(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/privacyidea/api/lib/postpolicy.py\", line 112, in policy_wrapper\r\n    response = wrapped_function(*args, **kwds)\r\n  [Previous line repeated 3 more times]\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/privacyidea/lib/event.py\", line 134, in event_wrapper\r\n    event_handler.do(e_handler_def.get(\"action\"),\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/privacyidea/lib/eventhandler/webhookeventhandler.py\", line 133, in do\r\n    user = User(login=g.logged_in_user.get('username'),\r\n  File \"/opt/privacyidea/lib/python3.9/site-packages/werkzeug/local.py\", line 347, in __getattr__\r\n    return getattr(self._get_current_object(), name)\r\nAttributeError: '_AppCtxGlobals' object has no attribute 'logged_in_user'\r\n~~~~\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\n# License:  AGPLv3\n# (c)\n#\n# This code is free software; you can redistribute it and/or\n# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE\n# License as published by the Free Software Foundation; either\n# version 3 of the License, or any later version.\n#\n# This code is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU AFFERO GENERAL PUBLIC LICENSE for more details.\n#\n# You should have received a copy of the GNU Affero General Public\n# License along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\n#\n__doc__ = \"\"\"This is th event handler module for posting webhooks.\nYou can send an webhook to trigger an event on an other system or use to replace\napi requests and reduce your traffic this way.\n\n\"\"\"\n\nfrom privacyidea.lib.eventhandler.base import BaseEventHandler\nfrom privacyidea.lib import _\nimport json\nimport logging\nimport requests\nfrom requests.exceptions import HTTPError, Timeout, ConnectionError, RequestException\nfrom privacyidea.lib.user import User\nfrom privacyidea.lib.utils import replace_function_event_handler\n\n\nlog = logging.getLogger(__name__)\nTIMEOUT = 10\n\n\nclass CONTENT_TYPE(object):\n    \"\"\"\n    Allowed type off content\n    \"\"\"\n    JSON = \"json\"\n    URLENCODED = \"urlendcode\"\n\n\nclass ACTION_TYPE(object):\n    \"\"\"\n    Allowed actions\n    \"\"\"\n    POST_WEBHOOK = \"post_webhook\"\n\n\nclass WebHookHandler(BaseEventHandler):\n    \"\"\"\n    With the WebHook Handler, a webhook can be sent at a particular event\n    \"\"\"\n\n    identifier = \"WebHookHandler\"\n    description = \"This eventhandler can post webhooks\"\n\n    @property\n    def allowed_positions(cls):\n        \"\"\"\n        This returns the allowed positions of the event handler definition.\n\n        :return: list of allowed positions\n        \"\"\"\n        return [\"post\", \"pre\"]\n\n    @property\n    def actions(cls):\n        \"\"\"\n        This method returns a dictionary of allowed actions and possible\n        options in this handler module.\n\n        :return: dict with actions\n        \"\"\"\n        # The event handler has just one action. Maybe we can  hide action select for the clarity of the UI\n        actions = {ACTION_TYPE.POST_WEBHOOK: {\n            \"URL\": {\n                \"type\": \"str\",\n                \"required\": True,\n                \"description\": _(\"The URL the WebHook is posted to\")\n            },\n            \"content_type\": {\n                \"type\": \"str\",\n                \"required\": True,\n                \"description\": _(\"The encoding that is sent to the WebHook, for example json\"),\n                \"value\": [\n                    CONTENT_TYPE.JSON,\n                    CONTENT_TYPE.URLENCODED]\n            },\n            \"replace\": {\n                \"type\": \"bool\",\n                \"required\": True,\n                \"description\": _(\"You can replace placeholder like {logged_in_user}\")\n            },\n            \"data\": {\n                \"type\": \"str\",\n                \"required\": True,\n                \"description\": _(\"The data posted in the WebHook\")\n            }\n        }}\n        return actions\n\n    def do(self, action, options=None):\n        \"\"\"\n        This method executes the defined action in the given event.\n\n        :param action: The action to perform\n        :type action: str\n        :param options: Contains the flask parameters g, URL, data, parameter and the fire and forget\n        :type options: dict\n        :return:\n        \"\"\"\n        ret = True\n        g = options.get(\"g\")\n        request = options.get(\"request\")\n        handler_def = options.get(\"handler_def\")\n        handler_options = handler_def.get(\"options\")\n        webhook_url = handler_options.get(\"URL\")\n        webhook_text = handler_options.get(\"data\")\n        content_type = handler_options.get(\"content_type\")\n        replace = handler_options.get(\"replace\")\n        token_serial = None\n        tokenowner = None\n\n        if request is not None:\n            token_serial = request.all_data.get('serial')\n            tokenowner = self._get_tokenowner(request)\n        user = User(login=g.logged_in_user.get('username'),\n                    realm=g.logged_in_user.get('realm'))\n\n        if replace:\n            webhook_text = replace_function_event_handler(webhook_text, token_serial=token_serial,\n                                                          tokenowner=tokenowner, logged_in_user=user)\n\n        if action.lower() == ACTION_TYPE.POST_WEBHOOK:\n            try:\n                if content_type == CONTENT_TYPE.JSON:\n                    \"\"\"\n                    This is the main function where the webhook is sent.\n                    Documentation of the requests function is found her docs.python-requests.org\n                    \"\"\"\n                    log.info('A webhook is send to {0!r} with the text: {1!r}'.format(\n                        webhook_url, webhook_text))\n                    resp = requests.post(webhook_url, data=json.dumps(webhook_text),\n                                         headers={'Content-Type': content_type}, timeout=TIMEOUT)\n                    # all answers will be logged in the debug. The HTTP response code will be shown in the audit too\n                    log.info(resp.status_code)\n                    log.debug(resp)\n                elif content_type == CONTENT_TYPE.URLENCODED:\n                    \"\"\"\n                    This is the main function where the webhook is sent.\n                    Documentation of the requests function is found her docs.python-requests.org\n                    \"\"\"\n                    log.info('A webhook is send to {0!r} with the text: {1!r}'.format(\n                        webhook_url, webhook_text))\n                    resp = requests.post(webhook_url, webhook_text,\n                                         headers={'Content-Type': content_type}, timeout=TIMEOUT)\n                    # all answers will be logged in the debug. The HTTP response code will be shown in the audit too\n                    log.info(resp.status_code)\n                    log.debug(resp)\n                else:\n                    log.warning('Unknown content type value: {0!s}'.format(content_type))\n                    ret = False\n            # error handling\n            except (HTTPError, ConnectionError, RequestException, Timeout) as err:\n                log.warning(err)\n                return False\n        else:\n            log.warning('Unknown action value: {0!s}'.format(action))\n            ret = False\n        return ret\n", "path": "privacyidea/lib/eventhandler/webhookeventhandler.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\n# License:  AGPLv3\n# (c)\n#\n# This code is free software; you can redistribute it and/or\n# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE\n# License as published by the Free Software Foundation; either\n# version 3 of the License, or any later version.\n#\n# This code is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU AFFERO GENERAL PUBLIC LICENSE for more details.\n#\n# You should have received a copy of the GNU Affero General Public\n# License along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\n#\n__doc__ = \"\"\"This is th event handler module for posting webhooks.\nYou can send an webhook to trigger an event on an other system or use to replace\napi requests and reduce your traffic this way.\n\n\"\"\"\n\nfrom privacyidea.lib.eventhandler.base import BaseEventHandler\nfrom privacyidea.lib import _\nimport json\nimport logging\nimport requests\nfrom requests.exceptions import HTTPError, Timeout, ConnectionError, RequestException\nfrom privacyidea.lib.user import User\nfrom privacyidea.lib.utils import replace_function_event_handler\nfrom privacyidea.lib.error import UserError\n\n\nlog = logging.getLogger(__name__)\nTIMEOUT = 10\n\n\nclass CONTENT_TYPE(object):\n    \"\"\"\n    Allowed type off content\n    \"\"\"\n    JSON = \"json\"\n    URLENCODED = \"urlendcode\"\n\n\nclass ACTION_TYPE(object):\n    \"\"\"\n    Allowed actions\n    \"\"\"\n    POST_WEBHOOK = \"post_webhook\"\n\n\nclass WebHookHandler(BaseEventHandler):\n    \"\"\"\n    With the WebHook Handler, a webhook can be sent at a particular event\n    \"\"\"\n\n    identifier = \"WebHookHandler\"\n    description = \"This eventhandler can post webhooks\"\n\n    @property\n    def allowed_positions(cls):\n        \"\"\"\n        This returns the allowed positions of the event handler definition.\n\n        :return: list of allowed positions\n        \"\"\"\n        return [\"post\", \"pre\"]\n\n    @property\n    def actions(cls):\n        \"\"\"\n        This method returns a dictionary of allowed actions and possible\n        options in this handler module.\n\n        :return: dict with actions\n        \"\"\"\n        # The event handler has just one action. Maybe we can  hide action select for the clarity of the UI\n        actions = {ACTION_TYPE.POST_WEBHOOK: {\n            \"URL\": {\n                \"type\": \"str\",\n                \"required\": True,\n                \"description\": _(\"The URL the WebHook is posted to\")\n            },\n            \"content_type\": {\n                \"type\": \"str\",\n                \"required\": True,\n                \"description\": _(\"The encoding that is sent to the WebHook, for example json\"),\n                \"value\": [\n                    CONTENT_TYPE.JSON,\n                    CONTENT_TYPE.URLENCODED]\n            },\n            \"replace\": {\n                \"type\": \"bool\",\n                \"required\": True,\n                \"description\": _(\"You can replace placeholder like {logged_in_user}\")\n            },\n            \"data\": {\n                \"type\": \"str\",\n                \"required\": True,\n                \"description\": _(\"The data posted in the WebHook\")\n            }\n        }}\n        return actions\n\n    def do(self, action, options=None):\n        \"\"\"\n        This method executes the defined action in the given event.\n\n        :param action: The action to perform\n        :type action: str\n        :param options: Contains the flask parameters g, URL, data, parameter and the fire and forget\n        :type options: dict\n        :return:\n        \"\"\"\n        ret = True\n        g = options.get(\"g\")\n        request = options.get(\"request\")\n        handler_def = options.get(\"handler_def\")\n        handler_options = handler_def.get(\"options\")\n        webhook_url = handler_options.get(\"URL\")\n        webhook_text = handler_options.get(\"data\")\n        content_type = handler_options.get(\"content_type\")\n        replace = handler_options.get(\"replace\")\n        token_serial = None\n        tokenowner = None\n\n        if request is not None:\n            token_serial = request.all_data.get('serial')\n            tokenowner = self._get_tokenowner(request)\n\n        try:\n            user = User(login=g.logged_in_user.get('username'),\n                        realm=g.logged_in_user.get('realm'))\n        except (UserError, AttributeError) as e:  # pragma: no cover\n            log.info('Could not determine user: {0!s}'.format(e))\n            user = None\n\n        if replace:\n            webhook_text = replace_function_event_handler(webhook_text, token_serial=token_serial,\n                                                          tokenowner=tokenowner, logged_in_user=user)\n\n        if action.lower() == ACTION_TYPE.POST_WEBHOOK:\n            try:\n                if content_type == CONTENT_TYPE.JSON:\n                    \"\"\"\n                    This is the main function where the webhook is sent.\n                    Documentation of the requests function is found her docs.python-requests.org\n                    \"\"\"\n                    log.info('A webhook is send to {0!r} with the text: {1!r}'.format(\n                        webhook_url, webhook_text))\n                    resp = requests.post(webhook_url, data=json.dumps(webhook_text),\n                                         headers={'Content-Type': content_type}, timeout=TIMEOUT)\n                    # all answers will be logged in the debug. The HTTP response code will be shown in the audit too\n                    log.info(resp.status_code)\n                    log.debug(resp)\n                elif content_type == CONTENT_TYPE.URLENCODED:\n                    \"\"\"\n                    This is the main function where the webhook is sent.\n                    Documentation of the requests function is found her docs.python-requests.org\n                    \"\"\"\n                    log.info('A webhook is send to {0!r} with the text: {1!r}'.format(\n                        webhook_url, webhook_text))\n                    resp = requests.post(webhook_url, webhook_text,\n                                         headers={'Content-Type': content_type}, timeout=TIMEOUT)\n                    # all answers will be logged in the debug. The HTTP response code will be shown in the audit too\n                    log.info(resp.status_code)\n                    log.debug(resp)\n                else:\n                    log.warning('Unknown content type value: {0!s}'.format(content_type))\n                    ret = False\n            # error handling\n            except (HTTPError, ConnectionError, RequestException, Timeout) as err:\n                log.warning(err)\n                return False\n        else:\n            log.warning('Unknown action value: {0!s}'.format(action))\n            ret = False\n        return ret\n", "path": "privacyidea/lib/eventhandler/webhookeventhandler.py"}]}