Repository: privacyidea/privacyidea

Issue:
Display of multiple token serials in the audit log
It would be nice if multiple token serials could also be displayed in the audit log.



Discussion:

Resolving patch:
diff --git a/privacyidea/api/auth.py b/privacyidea/api/auth.py
--- a/privacyidea/api/auth.py
+++ b/privacyidea/api/auth.py
@@ -309,6 +309,8 @@ def get_auth_token():
                                                     superuser_realms=
                                                     superuser_realms)
         details = details or {}
+        serials = ",".join([challenge_info["serial"] for challenge_info in details["multi_challenge"]]) \
+            if 'multi_challenge' in details else details.get('serial')
         if db_admin_exist(loginname) and user_auth and realm == get_default_realm():
             # If there is a local admin with the same login name as the user
             # in the default realm, we inform about this in the log file.
@@ -321,14 +323,14 @@ def get_auth_token():
                                 "administrator": user_obj.login,
                                 "realm": user_obj.realm,
                                 "resolver": user_obj.resolver,
-                                "serial": details.get('serial', None),
+                                "serial": serials,
                                 "info": u"{0!s}|loginmode={1!s}".format(log_used_user(user_obj),
                                                                         details.get("loginmode"))})
         else:
             g.audit_object.log({"user": user_obj.login,
                                 "realm": user_obj.realm,
                                 "resolver": user_obj.resolver,
-                                "serial": details.get('serial', None),
+                                "serial": serials,
                                 "info": u"{0!s}|loginmode={1!s}".format(log_used_user(user_obj),
                                         details.get("loginmode"))})
 
diff --git a/privacyidea/api/validate.py b/privacyidea/api/validate.py
--- a/privacyidea/api/validate.py
+++ b/privacyidea/api/validate.py
@@ -416,10 +416,11 @@ def check():
                     # additional attributes
                     for k, v in ui.items():
                         result["attributes"][k] = v
-
+    serials = ",".join([challenge_info["serial"] for challenge_info in details["multi_challenge"]]) \
+        if 'multi_challenge' in details else details.get('serial')
     g.audit_object.log({"info": log_used_user(user, details.get("message")),
                         "success": success,
-                        "serial": serial or details.get("serial"),
+                        "serial": serials,
                         "token_type": details.get("type")})
     return send_result(result, rid=2, details=details)
 


Test patch:
diff --git a/tests/test_api_roles.py b/tests/test_api_roles.py
--- a/tests/test_api_roles.py
+++ b/tests/test_api_roles.py
@@ -11,16 +11,16 @@
 from . import ldap3mock
 from .test_api_validate import LDAPDirectory
 from .base import MyApiTestCase
-from privacyidea.lib.error import (TokenAdminError, UserError)
 from privacyidea.lib.token import (get_tokens, remove_token, enable_token,
-                                   assign_token, unassign_token, init_token)
+                                   assign_token, init_token)
 from privacyidea.lib.user import User
 from privacyidea.lib.tokenclass import AUTH_DATE_FORMAT
 from privacyidea.lib.resolver import save_resolver
 from privacyidea.models import Token
 from privacyidea.lib.realm import (set_realm, delete_realm, set_default_realm)
 from privacyidea.api.lib.postpolicy import DEFAULT_POLICY_TEMPLATE_URL
-from privacyidea.lib.policy import ACTION, SCOPE, set_policy, delete_policy, LOGINMODE
+from privacyidea.lib.policy import (ACTION, SCOPE, set_policy, delete_policy,
+                                    LOGINMODE, ACTIONVALUE)
 
 
 PWFILE = "tests/testdata/passwords"
@@ -175,6 +175,11 @@ def setUp(self):
         from privacyidea.lib.token import set_pin
         set_pin("hotp1", "pin")
 
+    def tearDown(self):
+        remove_token('hotp1')
+        delete_policy('pol_cr')
+        delete_policy('webuilog')
+
     def test_01_challenge_response_at_webui(self):
         with self.app.test_request_context('/auth',
                                            method='POST',
@@ -199,6 +204,29 @@ def test_01_challenge_response_at_webui(self):
             data = res.json
             self.assertEqual(data.get("result").get("value").get("username"), "selfservice")
 
+    def test_02_auth_chal_resp_multi_token(self):
+        # create another token
+        Token("hotp2", "hotp", otpkey=self.otpkey, userid=1004,
+              resolver=self.resolvername1, realm=self.realm1).save()
+        set_policy(name="pol_otppin", scope=SCOPE.AUTH,
+                   action="{0!s}={1!s}".format(ACTION.OTPPIN, ACTIONVALUE.USERSTORE))
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "selfservice",
+                                                 "password": 'test'}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 401, res)
+            result = res.json.get('detail')
+            self.assertTrue('multi_challenge' in result, result)
+            self.assertEqual({'hotp1', 'hotp2'}, set([x['serial'] for x in result.get('multi_challenge')]))
+
+        # check if we have both serials in the audit log
+        ae = self.find_most_recent_audit_entry(action='POST /auth')
+        self.assertTrue({"hotp1", "hotp2"}.issubset(set(ae.get('serial').split(','))), ae)
+
+        delete_policy('pol_otppin')
+        remove_token('hotp2')
+
 
 class APISelfserviceTestCase(MyApiTestCase):
 
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
@@ -2309,6 +2309,10 @@ def test_26_multiple_challenge_response(self):
                                    transaction_id).all()
         self.assertEqual(len(r), 2)
 
+        # check that both serials appear in the audit log
+        ae = self.find_most_recent_audit_entry(action='* /validate/check')
+        self.assertEqual({"CR2A", "CR2B"}, set(ae.get('serial').split(',')), ae)
+
         # Check the second response to the challenge, the second step in
         # challenge response:
 
@@ -2952,6 +2956,10 @@ def test_35_application_tokentype(self):
         triggered_serials = [item['serial'] for item in detail.get("multi_challenge")]
         self.assertTrue("tok_hotp" in triggered_serials and "tok_totp" in triggered_serials)
 
+        # check that both serials appear in the audit log
+        ae = self.find_most_recent_audit_entry(action='POST /validate/triggerchallenge')
+        self.assertTrue({"tok_hotp", "tok_totp"}.issubset(set(ae.get('serial').split(','))), ae)
+
         # Set a policy, that the application is allowed to specify tokentype
         set_policy(name="pol_application_tokentype",
                    scope=SCOPE.AUTHZ,