Repository: privacyidea/privacyidea

Issue:
Better separation of audit log from privacyidea*
Hi,

python logging may be easily separated using the qualname. However, privacyidea uses the module/class names. Since they all start with "privacyidea.", it is not possible to log the audit to one place and all the rest to a different place (python logging cannot *exclude* qualnames).

To solve this, one could use a custom qualname for the privacyidea audit. I think here:
https://github.com/privacyidea/privacyidea/blob/ea7d9e53d42504288ba3909f7057924fe8d250b0/privacyidea/lib/auditmodules/loggeraudit.py#L62

Best regards,

Henning




Discussion:
I would suggest "pi-audit"
The qualname should not be *another* *fixed* value.

It can be configured in pi.cfg via
``PI_AUDT_LOGGER_QUALNAME`` and defaults to ``__name__``.

The config entry needs to be added tot he documentation.

Resolving patch:
diff --git a/privacyidea/lib/auditmodules/loggeraudit.py b/privacyidea/lib/auditmodules/loggeraudit.py
--- a/privacyidea/lib/auditmodules/loggeraudit.py
+++ b/privacyidea/lib/auditmodules/loggeraudit.py
@@ -56,10 +56,9 @@
 """
 
 import logging
+import json
 from privacyidea.lib.auditmodules.base import (Audit as AuditBase)
-import datetime
-
-log = logging.getLogger(__name__)
+from datetime import datetime
 
 
 class Audit(AuditBase):
@@ -73,6 +72,8 @@ class Audit(AuditBase):
     def __init__(self, config=None):
         super(Audit, self).__init__(config)
         self.name = "loggeraudit"
+        self.qualname = self.config.get('PI_AUDIT_LOGGER_QUALNAME', __name__)
+        self.logger = logging.getLogger(self.qualname)
 
     def finalize_log(self):
         """
@@ -80,8 +81,6 @@ def finalize_log(self):
         e.g. write the data to a file.
         """
         self.audit_data["policies"] = ",".join(self.audit_data.get("policies", []))
-        self.audit_data["timestamp"] = datetime.datetime.utcnow()
-        log.info(u"{0!s}".format(self.audit_data))
+        self.audit_data["timestamp"] = datetime.utcnow().isoformat()
+        self.logger.info("{0!s}".format(json.dumps(self.audit_data, sort_keys=True)))
         self.audit_data = {}
-
-


Test patch:
diff --git a/tests/test_lib_audit.py b/tests/test_lib_audit.py
--- a/tests/test_lib_audit.py
+++ b/tests/test_lib_audit.py
@@ -6,7 +6,6 @@
 """
 import datetime
 import os
-import time
 
 from mock import mock
 
@@ -16,6 +15,7 @@
 from privacyidea.lib.auditmodules.loggeraudit import Audit as LoggerAudit
 from privacyidea.lib.auditmodules.sqlaudit import column_length
 from .base import MyTestCase, OverrideConfigTestCase
+from testfixtures import log_capture
 
 PUBLIC = "tests/testdata/public.pem"
 PRIVATE = "tests/testdata/private.pem"
@@ -119,20 +119,23 @@ def test_02_get_count(self):
         self.Audit.log({"action": "/validate/check",
                         "success": False})
         self.Audit.finalize_log()
-        time.sleep(2)
+
         # remember the current time for later
         current_timestamp = datetime.datetime.now()
 
-        self.Audit.log({"action": "/validate/check",
-                        "success": True})
-        self.Audit.finalize_log()
+        # create a new audit log entry 2 seconds after the previous ones
+        with mock.patch('privacyidea.models.datetime') as mock_dt:
+            mock_dt.now.return_value = current_timestamp + datetime.timedelta(seconds=2)
+            self.Audit.log({"action": "/validate/check",
+                            "success": True})
+            self.Audit.finalize_log()
 
-        # freeze time at ``current_timestamp`` + 0.5s.
+        # freeze time at ``current_timestamp`` + 2.5s.
         # This is necessary because when doing unit tests on a CI server,
         # things will sometimes go slower than expected, which will
         # cause the very last assertion to fail.
         with mock.patch('datetime.datetime') as mock_dt:
-            mock_dt.now.return_value = current_timestamp + datetime.timedelta(seconds=0.5)
+            mock_dt.now.return_value = current_timestamp + datetime.timedelta(seconds=2.5)
 
             # get 4 authentications
             r = self.Audit.get_count({"action": "/validate/check"})
@@ -148,8 +151,8 @@ def test_02_get_count(self):
             self.assertEqual(r, 1)
 
     def test_03_lib_search(self):
-        res = search(self.app.config, {"page": 1, "page_size": 10, "sortorder":
-            "asc"})
+        res = search(self.app.config, {"page": 1, "page_size": 10,
+                                       "sortorder": "asc"})
         self.assertTrue(res.get("count") == 0, res)
 
         res = search(self.app.config, {"timelimit": "-1d"})
@@ -244,8 +247,8 @@ def test_07_sign_and_verify(self):
         self.Audit.session.commit()
         # and check if we get a failed signature check
         audit_log = self.Audit.search({"user": u"kölbel"})
-        self.assertEquals(audit_log.total, 1)
-        self.assertEquals(audit_log.auditdata[0].get("sig_check"), "FAIL")
+        self.assertEqual(audit_log.total, 1)
+        self.assertEqual(audit_log.auditdata[0].get("sig_check"), "FAIL")
 
     def test_08_policies(self):
         self.Audit.log({"action": "validate/check"})
@@ -266,29 +269,29 @@ def test_09_check_external_audit_db(self):
         self.app.config["PI_AUDIT_SQL_URI"] = AUDIT_DB
         audit = getAudit(self.app.config)
         total = audit.get_count({})
-        self.assertEquals(total, 5)
+        self.assertEqual(total, 5)
         # check that we have old style signatures in the DB
         db_entries = audit.search_query({"user": "testuser"})
         db_entry = next(db_entries)
         self.assertTrue(db_entry.signature.startswith('213842441384'), db_entry)
         # by default, PI_CHECK_OLD_SIGNATURES is false and thus the signature check fails
         audit_log = audit.search({"user": "testuser"})
-        self.assertEquals(audit_log.total, 1)
-        self.assertEquals(audit_log.auditdata[0].get("sig_check"), "FAIL")
+        self.assertEqual(audit_log.total, 1)
+        self.assertEqual(audit_log.auditdata[0].get("sig_check"), "FAIL")
 
         # they validate correctly when PI_CHECK_OLD_SIGNATURES is true
         # we need to create a new audit object to enable the new config
         self.app.config['PI_CHECK_OLD_SIGNATURES'] = True
         audit = getAudit(self.app.config)
         total = audit.get_count({})
-        self.assertEquals(total, 5)
+        self.assertEqual(total, 5)
         audit_log = audit.search({"user": "testuser"})
-        self.assertEquals(audit_log.total, 1)
-        self.assertEquals(audit_log.auditdata[0].get("sig_check"), "OK")
+        self.assertEqual(audit_log.total, 1)
+        self.assertEqual(audit_log.auditdata[0].get("sig_check"), "OK")
         # except for entry number 4 where the 'realm' was added afterwards
         audit_log = audit.search({"realm": "realm1"})
-        self.assertEquals(audit_log.total, 1)
-        self.assertEquals(audit_log.auditdata[0].get("sig_check"), "FAIL")
+        self.assertEqual(audit_log.total, 1)
+        self.assertEqual(audit_log.auditdata[0].get("sig_check"), "FAIL")
         # TODO: add new audit entry and check for new style signature
         # remove the audit SQL URI from app config
         self.app.config.pop("PI_AUDIT_SQL_URI", None)
@@ -310,6 +313,7 @@ class Config(TestingConfig):
 
     def test_10_external_file_audit(self):
         a = LoggerAudit(config={})
+        self.assertFalse(a.is_readable)
         self.assertFalse(a.has_data)
         a.log({"action": "action1"})
         self.assertTrue(a.has_data)
@@ -329,6 +333,32 @@ def test_20_logger_audit(self):
         self.assertEqual(r.auditdata, [])
         self.assertEqual(r.total, 0)
 
+    @log_capture()
+    def test_30_logger_audit_qualname(self, capture):
+        # Check that the default qualname is 'privacyidea.lib.auditmodules.loggeraudit'
+        current_utc_time = datetime.datetime(2018, 3, 4, 5, 6, 8)
+        with mock.patch('privacyidea.lib.auditmodules.loggeraudit.datetime') as mock_dt:
+            mock_dt.utcnow.return_value = current_utc_time
+            a = LoggerAudit(config={})
+            a.log({"action": "No PI_AUDIT_LOGGER_QUALNAME given"})
+            a.finalize_log()
+            capture.check_present(
+                ('privacyidea.lib.auditmodules.loggeraudit', 'INFO',
+                 '{{"action": "No PI_AUDIT_LOGGER_QUALNAME given", "policies": "", '
+                 '"timestamp": "{timestamp}"}}'.format(timestamp=current_utc_time.isoformat())))
+
+        # Now change the qualname to 'pi-audit'
+        current_utc_time = datetime.datetime(2020, 3, 4, 5, 6, 8)
+        with mock.patch('privacyidea.lib.auditmodules.loggeraudit.datetime') as mock_dt:
+            mock_dt.utcnow.return_value = current_utc_time
+            a = LoggerAudit(config={"PI_AUDIT_LOGGER_QUALNAME": "pi-audit"})
+            a.log({"action": "PI_AUDIT_LOGGER_QUALNAME given"})
+            a.finalize_log()
+            capture.check_present(
+                ('pi-audit', 'INFO',
+                 '{{"action": "PI_AUDIT_LOGGER_QUALNAME given", "policies": "", '
+                 '"timestamp": "{timestamp}"}}'.format(timestamp=current_utc_time.isoformat())))
+
 
 class ContainerAuditTestCase(OverrideConfigTestCase):
     class Config(TestingConfig):