Repository: privacyidea/privacyidea

Issue:
/validate/triggerchallenge should write the triggered serials to the audit log
Currently, ``/validate/triggerchallenge`` only writes the user, resolver, realm and the number of triggered challenges to the audit log. It may be useful for helpdesk users to determine the tokens for which challenges have been triggered. Hence, the endpoint should write a list of triggered serials to the audit log.


Discussion:

Resolving patch:
diff --git a/privacyidea/api/validate.py b/privacyidea/api/validate.py
--- a/privacyidea/api/validate.py
+++ b/privacyidea/api/validate.py
@@ -530,12 +530,14 @@ def trigger_challenge():
     create_challenges_from_tokens(chal_resp_tokens, details, options)
     result_obj = len(details.get("multi_challenge"))
 
+    challenge_serials = [challenge_info["serial"] for challenge_info in details["multi_challenge"]]
     g.audit_object.log({
         "user": user.login,
         "resolver": user.resolver,
         "realm": user.realm,
         "success": result_obj > 0,
-        "info": log_used_user(user, "triggered {0!s} challenges".format(result_obj))
+        "info": log_used_user(user, "triggered {0!s} challenges".format(result_obj)),
+        "serial": ",".join(challenge_serials),
     })
 
     return send_result(result_obj, details=details)


Test patch:
diff --git a/tests/base.py b/tests/base.py
--- a/tests/base.py
+++ b/tests/base.py
@@ -176,6 +176,22 @@ def authenticate_selfservice_user(self):
             self.assertTrue(role == "user", result)
             self.assertEqual(result.get("value").get("realm"), "realm1")
 
+    def find_most_recent_audit_entry(self, **filter_data):
+        """
+        Given audit log entry filters, return the most recent entry matching the criteria,
+        or raise an IndexError if there is no such entry.
+        This is useful for testing the audit log behavior.
+        """
+        sorted_filter = filter_data.copy()
+        sorted_filter["sortorder"] = "desc"
+        with self.app.test_request_context('/audit/',
+                                           method='GET',
+                                           data=sorted_filter,
+                                           headers={"Authorization": self.at}):
+            res = self.app.full_dispatch_request()
+            # return the last entry
+            return res.json["result"]["value"]["auditdata"][0]
+
 
 class OverrideConfigTestCase(MyTestCase):
     """
diff --git a/tests/test_api_validate.py b/tests/test_api_validate.py
--- a/tests/test_api_validate.py
+++ b/tests/test_api_validate.py
@@ -3352,6 +3352,11 @@ def test_12_polltransaction(self):
             self.assertEqual(multichallenge[0].get("transaction_id"), transaction_id)
             self.assertEqual(multichallenge[1].get("transaction_id"), transaction_id)
 
+        # Check that serials are written to the audit log
+        entry = self.find_most_recent_audit_entry(action="*/validate/triggerchallenge*")
+        self.assertIn("tok1", entry["serial"])
+        self.assertIn("tok2", entry["serial"])
+
         # add a really old expired challenge for tok1
         old_transaction_id = "1111111111"
         old_challenge = Challenge(serial="tok1", transaction_id=old_transaction_id, challenge="")
@@ -3374,16 +3379,6 @@ def test_12_polltransaction(self):
             self.assertFalse(res.json["result"]["status"])
             self.assertIn("Missing parameter: 'transaction_id'", res.json["result"]["error"]["message"])
 
-        def _find_most_recent_polltransaction_in_audit():
-            with self.app.test_request_context('/audit/',
-                                               method='GET',
-                                               data={"action": "*/validate/polltransaction*",
-                                                     "sortorder": "desc"},
-                                               headers={"Authorization": self.at}):
-                res = self.app.full_dispatch_request()
-                # return the last entry
-                return res.json["result"]["value"]["auditdata"][0]
-
         # wildcards do not work
         with self.app.test_request_context("/validate/polltransaction", method="GET",
                                            data={"transaction_id": "*"}):
@@ -3401,7 +3396,7 @@ def _find_most_recent_polltransaction_in_audit():
             self.assertFalse(res.json["result"]["value"])
 
         # check audit log
-        entry = _find_most_recent_polltransaction_in_audit()
+        entry = self.find_most_recent_audit_entry(action="*/validate/polltransaction*")
         self.assertEqual(entry["info"], "transaction_id: 123456")
         self.assertEqual(entry["serial"], None)
         self.assertEqual(entry["user"], None)
@@ -3415,7 +3410,7 @@ def _find_most_recent_polltransaction_in_audit():
             self.assertFalse(res.json["result"]["value"])
 
         # but audit log contains both serials and the user
-        entry = _find_most_recent_polltransaction_in_audit()
+        entry = self.find_most_recent_audit_entry(action="*/validate/polltransaction*")
         self.assertEqual(entry["info"], "transaction_id: {}".format(transaction_id))
         self.assertIn("tok1", entry["serial"])
         self.assertIn("tok2", entry["serial"])
@@ -3433,7 +3428,7 @@ def _find_most_recent_polltransaction_in_audit():
             self.assertFalse(res.json["result"]["value"])
 
         # and the audit log contains no serials and the user
-        entry = _find_most_recent_polltransaction_in_audit()
+        entry = self.find_most_recent_audit_entry(action="*/validate/polltransaction*")
         self.assertEqual(entry["info"], "transaction_id: {}".format(old_transaction_id))
         self.assertEqual(entry["serial"], None)
         self.assertFalse(entry["success"])
@@ -3450,7 +3445,7 @@ def _find_most_recent_polltransaction_in_audit():
             self.assertTrue(res.json["result"]["status"])
             self.assertTrue(res.json["result"]["value"])
 
-        entry = _find_most_recent_polltransaction_in_audit()
+        entry = self.find_most_recent_audit_entry(action="*/validate/polltransaction*")
         self.assertEqual(entry["info"], "transaction_id: {}".format(transaction_id))
         # tok2 is not written to the audit log
         self.assertEqual(entry["serial"], "tok1")