Repository: privacyidea/privacyidea

Issue:
Additional Event Condition: Result->Authentication
Add a new condition for event handlers:

    result->authentication

This helps to distinguish between SUCCESS, FAILED, CHALLENGE


Discussion:

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
@@ -41,7 +41,7 @@
 from privacyidea.lib.utils import (compare_condition, compare_value_value,
                                    compare_generic_condition,
                                    parse_time_offset_from_now, is_true,
-                                   check_ip_in_policy)
+                                   check_ip_in_policy, AUTH_RESPONSE)
 import datetime
 from dateutil.tz import tzlocal
 import re
@@ -72,6 +72,7 @@ class CONDITION(object):
     DETAIL_MESSAGE = "detail_message"
     RESULT_VALUE = "result_value"
     RESULT_STATUS = "result_status"
+    RESULT_AUTHENTICATION = "result_authentication"
     TOKENREALM = "tokenrealm"
     TOKENRESOLVER = "tokenresolver"
     REALM = "realm"
@@ -198,6 +199,12 @@ def conditions(cls):
                 "value": ("True", "False"),
                 "group": GROUP.GENERAL
             },
+            CONDITION.RESULT_AUTHENTICATION: {
+                "type": "str",
+                "desc": _("The result.authentication within the response is the given value."),
+                "value": (AUTH_RESPONSE.ACCEPT, AUTH_RESPONSE.REJECT, AUTH_RESPONSE.CHALLENGE, AUTH_RESPONSE.DECLINED),
+                "group": GROUP.GENERAL
+            },
             "token_locked": {
                 "type": "str",
                 "desc": _("Check if the max failcounter of the token is "
@@ -444,6 +451,12 @@ def check_condition(self, options):
             if is_true(condition_value) != is_true(result_status):
                 return False
 
+        if CONDITION.RESULT_AUTHENTICATION in conditions:
+            condition_value = conditions.get(CONDITION.RESULT_AUTHENTICATION)
+            result_auth = content.get("result", {}).get("authentication")
+            if condition_value != result_auth:
+                return False
+
         # checking of max-failcounter state of the token
         if "token_locked" in conditions:
             if token_obj:
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
@@ -64,6 +64,18 @@
                           "s": string.punctuation}    # special
 
 
+class AUTH_RESPONSE(object):
+    __doc__ = """The return value in "result->authentication".
+    CHALLENGE indicates the start of a challenge and
+    DECLINED indicates a declined challenge, which e.g. happens with PUSH tokens, if the user declines the
+    authentication request.
+    """
+    ACCEPT = "ACCEPT"
+    REJECT = "REJECT"
+    CHALLENGE = "CHALLENGE"
+    DECLINED = "DECLINED"
+
+
 def check_time_in_range(time_range, check_time=None):
     """
     Check if the given time is contained in the time_range string.
@@ -1290,19 +1302,16 @@ def prepare_result(obj, rid=1, details=None):
         details["threadid"] = threading.current_thread().ident
         res["detail"] = details
 
-    # Fix for sending an information about challenge response
-    # TODO: Make this default, when we move from the binary result->value to
-    #       more states in version 4.0
     if rid > 1:
         if obj:
-            r_authentication = "ACCEPT"
+            r_authentication = AUTH_RESPONSE.ACCEPT
         elif not obj and details.get("multi_challenge"):
             # We have a challenge authentication
-            r_authentication = "CHALLENGE"
+            r_authentication = AUTH_RESPONSE.CHALLENGE
         elif not obj and (details.get("challenge_status") == "declined"):
-            r_authentication = "DECLINED"
+            r_authentication = AUTH_RESPONSE.DECLINED
         else:
-            r_authentication = "REJECT"
+            r_authentication = AUTH_RESPONSE.REJECT
         res["result"]["authentication"] = r_authentication
 
     return res


Test patch:
diff --git a/tests/test_lib_eventhandler_usernotification.py b/tests/test_lib_eventhandler_usernotification.py
--- a/tests/test_lib_eventhandler_usernotification.py
+++ b/tests/test_lib_eventhandler_usernotification.py
@@ -24,7 +24,7 @@
 from privacyidea.lib.token import init_token, unassign_token, remove_token
 from privacyidea.lib.tokenclass import DATE_FORMAT
 from privacyidea.lib.user import User, create_user
-from privacyidea.lib.utils import to_unicode
+from privacyidea.lib.utils import to_unicode, AUTH_RESPONSE
 from privacyidea.models import TokenOwner
 from . import smtpmock
 from .base import MyTestCase, FakeFlaskG, FakeAudit
@@ -232,7 +232,7 @@ def test_05_check_conditions(self):
         uhandler = UserNotificationEventHandler()
         resp = Response()
         # The actual result_status is false and the result_value is false.
-        resp.data = """{"result": {"value": false, "status": false}}"""
+        resp.data = """{"result": {"value": false, "authentication": "REJECT", "status": false}}"""
         builder = EnvironBuilder(method='POST')
         env = builder.get_environ()
         req = Request(env)
@@ -245,6 +245,22 @@ def test_05_check_conditions(self):
              "request": req})
         self.assertEqual(r, False)
 
+        # We expect the result_authentication to be "REJECT" and it is
+        r = uhandler.check_condition(
+            {"g": {},
+             "handler_def": {"conditions": {"result_authentication": AUTH_RESPONSE.REJECT}},
+             "response": resp,
+             "request": req})
+        self.assertTrue(r)
+
+        # We expect the result_authentication to be "ACCEPT" and it is not
+        r = uhandler.check_condition(
+            {"g": {},
+             "handler_def": {"conditions": {"result_authentication": AUTH_RESPONSE.ACCEPT}},
+             "response": resp,
+             "request": req})
+        self.assertFalse(r)
+
         # We expect the result_value to be True, but it is not.
         r = uhandler.check_condition(
             {"g": {},