Repository: privacyidea/privacyidea

Issue:
Event Handler fails if detail->message or detail->error->message is used and is missing from the object
I am setting up Event Handlers utilizing the detail->message and detail->error->message items.  These objects do not always exist in every response.  If a handler is setup to use them, but the response does not contain the message/error message, an unhandled exception occurs and the process in privacyIDEA dies/fails.

This is very bad if the handler is accidentally attached to auth with a pre position as it will cause all subsequent auths to fail and the event handler has to be disabled via the database directly to re-access the system.

I was able to patch the privacyidea/lib/eventhandler/base.py code to fix it by adding an **or ""** to the end of the content.get statements for both conditions:

        if CONDITION.DETAIL_ERROR_MESSAGE in conditions:
            message = content.get("detail", {}).get("error", {}).get("message") or ""
            search_exp = conditions.get(CONDITION.DETAIL_ERROR_MESSAGE)
            m = re.search(search_exp, message)
            if not bool(m):
                return False

        if CONDITION.DETAIL_MESSAGE in conditions:
            message = content.get("detail", {}).get("message") or ""
            search_exp = conditions.get(CONDITION.DETAIL_MESSAGE)
            m = re.search(search_exp, message)
            if not bool(m):
                return False


### Steps to reproduce

1.  Create event handler that uses either detail_message or detail_error_message
2.  Set Events to Auth
3.  Set Position to Pre

### Expected outcome

The event handler should be skipped because the detail message does not exist in the response.

### Actual outcome

privacyIDEA no longer allows logins and all attempts fail.  No error is output to the log file.

### Configuration

* **privacyIDEA version**: 3.1.1
* **Installation method**: Ubuntu
* **Python version**: 2.7
* **Operating system**: 16.04 LTS
* **Webserver**: Apache2
* **Token database**: MySQL



Discussion:
You say your expected outcome is that the "event handler should be skipped".
I.e. you expect the action not to be triggered?

Is there a reason you did not do it this way (I guess the message will not be ``None``)

~~~~python
        message = content.get("detail", {}).get("message", "")
~~~~

By the way: Your event handler definition makes no sense, since the never-ever will be a response available in the PRE events!
Yes, I expect the action to not be triggered.  I also understand that my event handler is nonsensical as it would never have a response.

My reasoning for submitting this issue is due to the fact that if you setup an event handler like I said, most likely by accident, it causes an unhandled exception to occur which causes all calls to the /auth endpoint to fail.   I do not believe any configuration, no matter how nonsensical or invalid, should cause the entire application to fail/crash.

As for why I used an or "" and not add a default value to get(), there's no reason.  I did not view the defintion of the get() function to determine alternatives.  All I knew was that message being equal to None was causing the regex search to throw an exception which was not handled. 
I however see this a bit different. Since I think a wrong configuration should not necessarily go silent. We had this aspect at different points, where wrong configuration can cause faulty requests.
And most of the time I think this makes sense, since if an admin *thinks* he configures a certain behaviour but an event will never trigger an action, since the definition is faulty and the admin will never realize, since a default passing behaviour is set, then this is also a non direable behaviour. 

I think in this case we will check, if there are reasonable scenarios, where this event definition would actually make sense but the details might not be available. (I am thinking of the policy "no_details_on_success"...)

So after checking this, we may add such a fail safe.
# TODO

Check for logic conistancy with missing details and a consistant/logical behavious with other possibly missing conditions in event conditions.
Yes, that is the concern I have myself and why I have not enabled the proper 'post' event handler in my production environment.  I know that detail_error_message does not exist on all /auth or /validate/check requests, but I'm also not sure if it EVER exists for those endpoints.

It also makes me wonder if details_message will always exist in the object on every 'post' for /auth and /validate/check.  I'm not sure if you can confirm that for me or not.

I think I would rather have the exception in the event handler caught, and a log entry created, but the /auth or /validate/check endpoint would still complete successfully.  This would make the invalid configuration not be silent, but also increase the reliability and stability of the application.
I checked the occurrences of ``content.get(`` in the event handler conditions the [function check_conditions](https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/eventhandler/base.py#L327).

And the two other ``content.get()`` actually in the eventhandler case work this way, if the searched content does not exist, it evaluates to False.
So I am happy to got with @droobah's suggestion.


Resolving patch:
diff --git a/privacyidea/lib/eventhandler/base.py b/privacyidea/lib/eventhandler/base.py
--- a/privacyidea/lib/eventhandler/base.py
+++ b/privacyidea/lib/eventhandler/base.py
@@ -451,14 +451,14 @@ def check_condition(self, options):
                 return False
 
         if CONDITION.DETAIL_ERROR_MESSAGE in conditions:
-            message = content.get("detail", {}).get("error", {}).get("message")
+            message = content.get("detail", {}).get("error", {}).get("message", "")
             search_exp = conditions.get(CONDITION.DETAIL_ERROR_MESSAGE)
             m = re.search(search_exp, message)
             if not bool(m):
                 return False
 
         if CONDITION.DETAIL_MESSAGE in conditions:
-            message = content.get("detail", {}).get("message")
+            message = content.get("detail", {}).get("message", "")
             search_exp = conditions.get(CONDITION.DETAIL_MESSAGE)
             m = re.search(search_exp, message)
             if not bool(m):


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
@@ -440,6 +440,23 @@ def test_05_detail_messages_condition(self):
         )
         self.assertEqual(r, False)
 
+        # Check DETAIL_MESSAGE to evaluate to False if it does not exist
+        resp = Response()
+        resp.data = """{"result": {"value": true, "status": true},
+                "detail": {"options": "Nothing"}
+                }
+                """
+
+        r = uhandler.check_condition(
+            {"g": {},
+             "handler_def": {"conditions": {CONDITION.DETAIL_MESSAGE:
+                                                "special"}},
+             "request": req,
+             "response": resp
+             }
+        )
+        self.assertEqual(r, False)
+
         # Check DETAIL_ERROR_MESSAGE
         resp = Response()
         resp.data = """{"result": {"value": false, "status": false},