dsdb: audit samdb and password changes
authorGary Lockyer <gary@catalyst.net.nz>
Tue, 3 Apr 2018 23:59:41 +0000 (11:59 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Sat, 9 Jun 2018 13:02:11 +0000 (15:02 +0200)
Add audit logging of DSDB operations and password changes, log messages
are logged in human readable format and if samba is commpile with
JANSSON support in JSON format.

Log:
  * Details all DSDB add, modify and delete operations. Logs
    attributes, values, session details, transaction id.
  * Transaction roll backs.
  * Prepare commit and commit failures.
  * Summary details of replicated updates.

Signed-off-by: Gary Lockyer <gary@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
12 files changed:
python/samba/tests/audit_log_base.py [new file with mode: 0644]
python/samba/tests/audit_log_dsdb.py [new file with mode: 0644]
python/samba/tests/audit_log_pass_change.py [new file with mode: 0644]
selftest/target/Samba4.pm
source4/dsdb/samdb/ldb_modules/audit_log.c [new file with mode: 0644]
source4/dsdb/samdb/ldb_modules/audit_util.c [new file with mode: 0644]
source4/dsdb/samdb/ldb_modules/samba_dsdb.c
source4/dsdb/samdb/ldb_modules/tests/test_audit_log.c [new file with mode: 0644]
source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c [new file with mode: 0644]
source4/dsdb/samdb/ldb_modules/wscript_build
source4/dsdb/samdb/ldb_modules/wscript_build_server
source4/selftest/tests.py

diff --git a/python/samba/tests/audit_log_base.py b/python/samba/tests/audit_log_base.py
new file mode 100644 (file)
index 0000000..fa51911
--- /dev/null
@@ -0,0 +1,177 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from __future__ import print_function
+"""Tests for DSDB audit logging.
+"""
+
+import samba.tests
+from samba.messaging import Messaging
+from samba.dcerpc.messaging import MSG_AUTH_LOG, AUTH_EVENT_NAME
+import time
+import json
+import os
+import re
+
+def getAudit(message):
+    if "type" not in message:
+        return None
+
+    type = message["type"]
+    audit = message[type]
+    return audit
+
+class AuditLogTestBase(samba.tests.TestCase):
+
+
+    def setUp(self):
+        super(AuditLogTestBase, self).setUp()
+        lp_ctx = self.get_loadparm()
+        self.msg_ctx = Messaging((1,), lp_ctx=lp_ctx)
+        self.msg_ctx.irpc_add_name(self.event_type)
+
+        #
+        # Check the remote address of a message against the one beimg used
+        # for the tests.
+        #
+        def isRemote(message):
+            audit = getAudit(message)
+            if audit is None:
+                return false
+
+            remote = audit["remoteAddress"]
+            if remote is None:
+                return False
+
+            try:
+                addr = remote.split(":")
+                return addr[1] == self.remoteAddress
+            except IndexError:
+                return False
+
+        def messageHandler(context, msgType, src, message):
+            # This does not look like sub unit output and it
+            # makes these tests much easier to debug.
+            print(message)
+            jsonMsg = json.loads(message)
+            if ((jsonMsg["type"] == "passwordChange" or
+                jsonMsg["type"] == "dsdbChange" or
+                jsonMsg["type"] == "groupChange") and
+                    isRemote(jsonMsg)):
+                context["messages"].append(jsonMsg)
+            elif jsonMsg["type"] == "dsdbTransaction":
+                context["txnMessage"] = jsonMsg
+
+        self.context = {"messages": [], "txnMessage": ""}
+        self.msg_handler_and_context = (messageHandler, self.context)
+        self.msg_ctx.register(self.msg_handler_and_context,
+                              msg_type=self.message_type)
+
+        self.msg_ctx.irpc_add_name(AUTH_EVENT_NAME)
+
+        def authHandler(context, msgType, src, message):
+            jsonMsg = json.loads(message)
+            if jsonMsg["type"] == "Authorization" and isRemote(jsonMsg):
+                # This does not look like sub unit output and it
+                # makes these tests much easier to debug.
+                print(message)
+                context["sessionId"] = jsonMsg["Authorization"]["sessionId"]
+                context["serviceDescription"] =\
+                    jsonMsg["Authorization"]["serviceDescription"]
+
+        self.auth_context = {"sessionId": "", "serviceDescription": ""}
+        self.auth_handler_and_context = (authHandler, self.auth_context)
+        self.msg_ctx.register(self.auth_handler_and_context,
+                              msg_type=MSG_AUTH_LOG)
+
+        self.discardMessages()
+
+        self.server = os.environ["SERVER"]
+        self.connection = None
+
+    def tearDown(self):
+        self.discardMessages()
+        self.msg_ctx.irpc_remove_name(self.event_type)
+        self.msg_ctx.irpc_remove_name(AUTH_EVENT_NAME)
+        if self.msg_handler_and_context:
+            self.msg_ctx.deregister(self.msg_handler_and_context,
+                                    msg_type=self.message_type)
+        if self.auth_handler_and_context:
+            self.msg_ctx.deregister(self.auth_handler_and_context,
+                                    msg_type=MSG_AUTH_LOG)
+
+    def haveExpected(self, expected, dn):
+        if dn is None:
+            return len(self.context["messages"]) >= expected
+        else:
+            received = 0
+            for msg in self.context["messages"]:
+                audit = getAudit(msg)
+                if audit["dn"].lower() == dn.lower():
+                    received += 1
+                    if received >= expected:
+                        return True
+            return False
+
+
+    def waitForMessages(self, number, connection=None, dn=None):
+        """Wait for all the expected messages to arrive
+        The connection is passed through to keep the connection alive
+        until all the logging messages have been received.
+        """
+
+        self.connection = connection
+
+        start_time = time.time()
+        while not self.haveExpected(number, dn):
+            self.msg_ctx.loop_once(0.1)
+            if time.time() - start_time > 1:
+                self.connection = None
+                print("Timed out")
+                return []
+
+        self.connection = None
+        if dn is None:
+            return self.context["messages"]
+
+        messages = []
+        for msg in self.context["messages"]:
+            audit = getAudit(msg)
+            if audit["dn"].lower() == dn.lower():
+                messages.append(msg)
+        return messages
+
+    # Discard any previously queued messages.
+    def discardMessages(self):
+        self.msg_ctx.loop_once(0.001)
+        while len(self.context["messages"]):
+            self.context["messages"] = []
+            self.msg_ctx.loop_once(0.001)
+
+    GUID_RE = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
+
+    #
+    # Is the supplied GUID string correctly formatted
+    #
+    def is_guid(self, guid):
+        return re.match(self.GUID_RE, guid)
+
+    def get_session(self):
+        return self.auth_context["sessionId"]
+
+    def get_service_description(self):
+        return self.auth_context["serviceDescription"]
diff --git a/python/samba/tests/audit_log_dsdb.py b/python/samba/tests/audit_log_dsdb.py
new file mode 100644 (file)
index 0000000..5329af4
--- /dev/null
@@ -0,0 +1,608 @@
+# Tests for SamDb password change audit logging.
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from __future__ import print_function
+"""Tests for the SamDb logging of password changes.
+"""
+
+import samba.tests
+from samba.dcerpc.messaging import MSG_DSDB_LOG, DSDB_EVENT_NAME
+from samba.samdb import SamDB
+from samba.auth import system_session
+import os
+import time
+from samba.tests.audit_log_base import AuditLogTestBase
+from samba.tests import delete_force
+from samba.net import Net
+import samba
+from samba.dcerpc import security, lsa
+
+USER_NAME = "auditlogtestuser"
+USER_PASS = samba.generate_random_password(32, 32)
+SECOND_USER_NAME = "auditlogtestuser02"
+SECOND_USER_PASS = samba.generate_random_password(32, 32)
+
+
+class AuditLogDsdbTests(AuditLogTestBase):
+
+    def setUp(self):
+        self.message_type = MSG_DSDB_LOG
+        self.event_type   = DSDB_EVENT_NAME
+        super(AuditLogDsdbTests, self).setUp()
+
+        self.remoteAddress = os.environ["CLIENT_IP"]
+        self.server_ip = os.environ["SERVER_IP"]
+
+        host = "ldap://%s" % os.environ["SERVER"]
+        self.ldb = SamDB(url=host,
+                         session_info=system_session(),
+                         credentials=self.get_credentials(),
+                         lp=self.get_loadparm())
+        self.server = os.environ["SERVER"]
+
+        # Gets back the basedn
+        self.base_dn = self.ldb.domain_dn()
+
+        # Get the old "dSHeuristics" if it was set
+        dsheuristics = self.ldb.get_dsheuristics()
+
+        # Set the "dSHeuristics" to activate the correct "userPassword"
+        # behaviour
+        self.ldb.set_dsheuristics("000000001")
+
+        # Reset the "dSHeuristics" as they were before
+        self.addCleanup(self.ldb.set_dsheuristics, dsheuristics)
+
+        # Get the old "minPwdAge"
+        minPwdAge = self.ldb.get_minPwdAge()
+
+        # Set it temporarily to "0"
+        self.ldb.set_minPwdAge("0")
+        self.base_dn = self.ldb.domain_dn()
+
+        # Reset the "minPwdAge" as it was before
+        self.addCleanup(self.ldb.set_minPwdAge, minPwdAge)
+
+        # (Re)adds the test user USER_NAME with password USER_PASS
+        delete_force(self.ldb, "cn=" + USER_NAME + ",cn=users," + self.base_dn)
+        delete_force(
+            self.ldb,
+            "cn=" + SECOND_USER_NAME + ",cn=users," + self.base_dn)
+        self.ldb.add({
+            "dn": "cn=" + USER_NAME + ",cn=users," + self.base_dn,
+            "objectclass": "user",
+            "sAMAccountName": USER_NAME,
+            "userPassword": USER_PASS
+        })
+
+    #
+    # Discard the messages from the setup code
+    #
+    def discardSetupMessages(self, dn):
+        messages = self.waitForMessages(2, dn=dn)
+        self.discardMessages()
+
+
+    def tearDown(self):
+        self.discardMessages()
+        super(AuditLogDsdbTests, self).tearDown()
+
+    def waitForTransaction(self, connection=None):
+        """Wait for a transaction message to arrive
+        The connection is passed through to keep the connection alive
+        until all the logging messages have been received.
+        """
+
+        self.connection = connection
+
+        start_time = time.time()
+        while self.context["txnMessage"] == "":
+            self.msg_ctx.loop_once(0.1)
+            if time.time() - start_time > 1:
+                self.connection = None
+                return ""
+
+        self.connection = None
+        return self.context["txnMessage"]
+
+    def test_net_change_password(self):
+
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        creds = self.insta_creds(template=self.get_credentials())
+
+        lp = self.get_loadparm()
+        net = Net(creds, lp, server=self.server)
+        password = "newPassword!!42"
+
+        net.change_password(newpassword=password.encode('utf-8'),
+                            username=USER_NAME,
+                            oldpassword=USER_PASS)
+
+        messages = self.waitForMessages(1, net, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["dsdbChange"]
+        self.assertEquals("Modify", audit["operation"])
+        self.assertFalse(audit["performedAsSystem"])
+        self.assertTrue(dn.lower(), audit["dn"].lower())
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "DCE/RPC")
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+
+        attributes = audit["attributes"]
+        self.assertEquals(1, len(attributes))
+        actions = attributes["clearTextPassword"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertTrue(actions[0]["redacted"])
+        self.assertEquals("replace", actions[0]["action"])
+
+    def test_net_set_password(self):
+
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        creds = self.insta_creds(template=self.get_credentials())
+
+        lp = self.get_loadparm()
+        net = Net(creds, lp, server=self.server)
+        password = "newPassword!!42"
+        domain = lp.get("workgroup")
+
+        net.set_password(newpassword=password.encode('utf-8'),
+                         account_name=USER_NAME,
+                         domain_name=domain)
+        messages = self.waitForMessages(1, net, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+        audit = messages[0]["dsdbChange"]
+        self.assertEquals("Modify", audit["operation"])
+        self.assertFalse(audit["performedAsSystem"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "DCE/RPC")
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+
+        attributes = audit["attributes"]
+        self.assertEquals(1, len(attributes))
+        actions = attributes["clearTextPassword"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertTrue(actions[0]["redacted"])
+        self.assertEquals("replace", actions[0]["action"])
+
+    def test_ldap_change_password(self):
+
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        new_password = samba.generate_random_password(32, 32)
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: modify\n" +
+            "delete: userPassword\n" +
+            "userPassword: " + USER_PASS + "\n" +
+            "add: userPassword\n" +
+            "userPassword: " + new_password + "\n")
+
+        messages = self.waitForMessages(1)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["dsdbChange"]
+        self.assertEquals("Modify", audit["operation"])
+        self.assertFalse(audit["performedAsSystem"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "LDAP")
+
+        attributes = audit["attributes"]
+        self.assertEquals(1, len(attributes))
+        actions = attributes["userPassword"]["actions"]
+        self.assertEquals(2, len(actions))
+        self.assertTrue(actions[0]["redacted"])
+        self.assertEquals("delete", actions[0]["action"])
+        self.assertTrue(actions[1]["redacted"])
+        self.assertEquals("add", actions[1]["action"])
+
+    def test_ldap_replace_password(self):
+
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        new_password = samba.generate_random_password(32, 32)
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: modify\n" +
+            "replace: userPassword\n" +
+            "userPassword: " + new_password + "\n")
+
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["dsdbChange"]
+        self.assertEquals("Modify", audit["operation"])
+        self.assertFalse(audit["performedAsSystem"])
+        self.assertTrue(dn.lower(), audit["dn"].lower())
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "LDAP")
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+
+        attributes = audit["attributes"]
+        self.assertEquals(1, len(attributes))
+        actions = attributes["userPassword"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertTrue(actions[0]["redacted"])
+        self.assertEquals("replace", actions[0]["action"])
+
+    def test_ldap_add_user(self):
+
+        # The setup code adds a user, so we check for the dsdb events
+        # generated by it.
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        messages = self.waitForMessages(2, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(2,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[1]["dsdbChange"]
+        self.assertEquals("Add", audit["operation"])
+        self.assertFalse(audit["performedAsSystem"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "LDAP")
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+
+        attributes = audit["attributes"]
+        self.assertEquals(3, len(attributes))
+
+        actions = attributes["objectclass"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertEquals("add", actions[0]["action"])
+        self.assertEquals(1, len(actions[0]["values"]))
+        self.assertEquals("user", actions[0]["values"][0]["value"])
+
+        actions = attributes["sAMAccountName"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertEquals("add", actions[0]["action"])
+        self.assertEquals(1, len(actions[0]["values"]))
+        self.assertEquals(USER_NAME, actions[0]["values"][0]["value"])
+
+        actions = attributes["userPassword"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertEquals("add", actions[0]["action"])
+        self.assertTrue(actions[0]["redacted"])
+
+    def test_samdb_delete_user(self):
+
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        self.ldb.deleteuser(USER_NAME)
+
+        messages = self.waitForMessages(2, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(2,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[1]["dsdbChange"]
+        self.assertEquals("Delete", audit["operation"])
+        self.assertFalse(audit["performedAsSystem"])
+        self.assertTrue(dn.lower(), audit["dn"].lower())
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "LDAP")
+
+    def test_net_set_password_user_without_permission(self):
+
+        self.ldb.newuser(SECOND_USER_NAME, SECOND_USER_PASS)
+
+        creds = self.insta_creds(
+            template=self.get_credentials(),
+            username=SECOND_USER_NAME,
+            userpass=SECOND_USER_PASS,
+            kerberos_state=None)
+
+        lp = self.get_loadparm()
+        net = Net(creds, lp, server=self.server)
+        password = "newPassword!!42"
+        domain = lp.get("workgroup")
+
+        #
+        # This operation should fail and trigger a transaction roll back.
+        #
+        try:
+            net.set_password(newpassword=password.encode('utf-8'),
+                             account_name=USER_NAME,
+                             domain_name=domain)
+            self.fail("Expected exception not thrown")
+        except Exception:
+            pass
+
+        message = self.waitForTransaction(net)
+
+        audit = message["dsdbTransaction"]
+        self.assertEquals("rollback", audit["action"])
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+
+    def test_create_and_delete_secret_over_lsa(self):
+
+        dn = "cn=Test Secret,CN=System," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        creds = self.insta_creds(template=self.get_credentials())
+        lsa_conn = lsa.lsarpc(
+            "ncacn_np:%s" % self.server,
+            self.get_loadparm(),
+            creds)
+        lsa_handle = lsa_conn.OpenPolicy2(
+            system_name="\\",
+            attr=lsa.ObjectAttribute(),
+            access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
+        secret_name = lsa.String()
+        secret_name.string = "G$Test"
+        lsa_conn.CreateSecret(
+            handle=lsa_handle,
+            name=secret_name,
+            access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
+
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["dsdbChange"]
+        self.assertEquals("Add", audit["operation"])
+        self.assertTrue(audit["performedAsSystem"])
+        self.assertTrue(dn.lower(), audit["dn"].lower())
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "DCE/RPC")
+        attributes = audit["attributes"]
+        self.assertEquals(2, len(attributes))
+
+        object_class = attributes["objectClass"]
+        self.assertEquals(1, len(object_class["actions"]))
+        action = object_class["actions"][0]
+        self.assertEquals("add", action["action"])
+        values = action["values"]
+        self.assertEquals(1, len(values))
+        self.assertEquals("secret", values[0]["value"])
+
+        cn = attributes["cn"]
+        self.assertEquals(1, len(cn["actions"]))
+        action = cn["actions"][0]
+        self.assertEquals("add", action["action"])
+        values = action["values"]
+        self.assertEquals(1, len(values))
+        self.assertEquals("Test Secret", values[0]["value"])
+
+        #
+        # Now delete the secret.
+        self.discardMessages()
+        h = lsa_conn.OpenSecret(
+            handle=lsa_handle,
+            name=secret_name,
+            access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
+
+        lsa_conn.DeleteObject(h)
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        dn = "cn=Test Secret,CN=System," + self.base_dn
+        audit = messages[0]["dsdbChange"]
+        self.assertEquals("Delete", audit["operation"])
+        self.assertTrue(audit["performedAsSystem"])
+        self.assertTrue(dn.lower(), audit["dn"].lower())
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "DCE/RPC")
+
+    def test_modify(self):
+
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        #
+        # Add an attribute value
+        #
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: modify\n" +
+            "add: carLicense\n" +
+            "carLicense: license-01\n")
+
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["dsdbChange"]
+        self.assertEquals("Modify", audit["operation"])
+        self.assertFalse(audit["performedAsSystem"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "LDAP")
+
+        attributes = audit["attributes"]
+        self.assertEquals(1, len(attributes))
+        actions = attributes["carLicense"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertEquals("add", actions[0]["action"])
+        values = actions[0]["values"]
+        self.assertEquals(1, len(values))
+        self.assertEquals("license-01", values[0]["value"])
+
+        #
+        # Add an another value to the attribute
+        #
+        self.discardMessages()
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: modify\n" +
+            "add: carLicense\n" +
+            "carLicense: license-02\n")
+
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+        attributes = messages[0]["dsdbChange"]["attributes"]
+        self.assertEquals(1, len(attributes))
+        actions = attributes["carLicense"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertEquals("add", actions[0]["action"])
+        values = actions[0]["values"]
+        self.assertEquals(1, len(values))
+        self.assertEquals("license-02", values[0]["value"])
+
+        #
+        # Add an another two values to the attribute
+        #
+        self.discardMessages()
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: modify\n" +
+            "add: carLicense\n" +
+            "carLicense: license-03\n" +
+            "carLicense: license-04\n")
+
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+        attributes = messages[0]["dsdbChange"]["attributes"]
+        self.assertEquals(1, len(attributes))
+        actions = attributes["carLicense"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertEquals("add", actions[0]["action"])
+        values = actions[0]["values"]
+        self.assertEquals(2, len(values))
+        self.assertEquals("license-03", values[0]["value"])
+        self.assertEquals("license-04", values[1]["value"])
+
+        #
+        # delete two values to the attribute
+        #
+        self.discardMessages()
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: delete\n" +
+            "delete: carLicense\n" +
+            "carLicense: license-03\n" +
+            "carLicense: license-04\n")
+
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+        attributes = messages[0]["dsdbChange"]["attributes"]
+        self.assertEquals(1, len(attributes))
+        actions = attributes["carLicense"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertEquals("delete", actions[0]["action"])
+        values = actions[0]["values"]
+        self.assertEquals(2, len(values))
+        self.assertEquals("license-03", values[0]["value"])
+        self.assertEquals("license-04", values[1]["value"])
+
+        #
+        # replace two values to the attribute
+        #
+        self.discardMessages()
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: delete\n" +
+            "replace: carLicense\n" +
+            "carLicense: license-05\n" +
+            "carLicense: license-06\n")
+
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+        attributes = messages[0]["dsdbChange"]["attributes"]
+        self.assertEquals(1, len(attributes))
+        actions = attributes["carLicense"]["actions"]
+        self.assertEquals(1, len(actions))
+        self.assertEquals("replace", actions[0]["action"])
+        values = actions[0]["values"]
+        self.assertEquals(2, len(values))
+        self.assertEquals("license-05", values[0]["value"])
+        self.assertEquals("license-06", values[1]["value"])
diff --git a/python/samba/tests/audit_log_pass_change.py b/python/samba/tests/audit_log_pass_change.py
new file mode 100644 (file)
index 0000000..a86f650
--- /dev/null
@@ -0,0 +1,324 @@
+# Tests for SamDb password change audit logging.
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from __future__ import print_function
+"""Tests for the SamDb logging of password changes.
+"""
+
+import samba.tests
+from samba.dcerpc.messaging import MSG_DSDB_PWD_LOG, DSDB_PWD_EVENT_NAME
+from samba.samdb import SamDB
+from samba.auth import system_session
+import os
+from samba.tests.audit_log_base import AuditLogTestBase
+from samba.tests import delete_force
+from samba.net import Net
+from ldb import ERR_INSUFFICIENT_ACCESS_RIGHTS
+
+USER_NAME = "auditlogtestuser"
+USER_PASS = samba.generate_random_password(32, 32)
+
+SECOND_USER_NAME = "auditlogtestuser02"
+SECOND_USER_PASS = samba.generate_random_password(32, 32)
+
+
+class AuditLogPassChangeTests(AuditLogTestBase):
+
+    def setUp(self):
+        self.message_type = MSG_DSDB_PWD_LOG
+        self.event_type   = DSDB_PWD_EVENT_NAME
+        super(AuditLogPassChangeTests, self).setUp()
+
+        self.remoteAddress = os.environ["CLIENT_IP"]
+        self.server_ip = os.environ["SERVER_IP"]
+
+        host = "ldap://%s" % os.environ["SERVER"]
+        self.ldb = SamDB(url=host,
+                         session_info=system_session(),
+                         credentials=self.get_credentials(),
+                         lp=self.get_loadparm())
+        self.server = os.environ["SERVER"]
+
+        # Gets back the basedn
+        self.base_dn = self.ldb.domain_dn()
+
+        # Get the old "dSHeuristics" if it was set
+        dsheuristics = self.ldb.get_dsheuristics()
+
+        # Set the "dSHeuristics" to activate the correct "userPassword"
+        # behaviour
+        self.ldb.set_dsheuristics("000000001")
+
+        # Reset the "dSHeuristics" as they were before
+        self.addCleanup(self.ldb.set_dsheuristics, dsheuristics)
+
+        # Get the old "minPwdAge"
+        minPwdAge = self.ldb.get_minPwdAge()
+
+        # Set it temporarily to "0"
+        self.ldb.set_minPwdAge("0")
+        self.base_dn = self.ldb.domain_dn()
+
+        # Reset the "minPwdAge" as it was before
+        self.addCleanup(self.ldb.set_minPwdAge, minPwdAge)
+
+        # (Re)adds the test user USER_NAME with password USER_PASS
+        delete_force(self.ldb, "cn=" + USER_NAME + ",cn=users," + self.base_dn)
+        delete_force(
+            self.ldb,
+            "cn=" + SECOND_USER_NAME + ",cn=users," + self.base_dn)
+        self.ldb.add({
+            "dn": "cn=" + USER_NAME + ",cn=users," + self.base_dn,
+            "objectclass": "user",
+            "sAMAccountName": USER_NAME,
+            "userPassword": USER_PASS
+        })
+
+    #
+    # Discard the messages from the setup code
+    #
+    def discardSetupMessages(self, dn):
+        messages = self.waitForMessages(1, dn=dn)
+        self.discardMessages()
+
+    def tearDown(self):
+        super(AuditLogPassChangeTests, self).tearDown()
+
+    def test_net_change_password(self):
+
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        creds = self.insta_creds(template=self.get_credentials())
+
+        lp = self.get_loadparm()
+        net = Net(creds, lp, server=self.server)
+        password = "newPassword!!42"
+
+        net.change_password(newpassword=password.encode('utf-8'),
+                            username=USER_NAME,
+                            oldpassword=USER_PASS)
+
+        messages = self.waitForMessages(1, net, dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+        audit = messages[0]["passwordChange"]
+        self.assertEquals("Change", audit["action"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "DCE/RPC")
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+
+    def test_net_set_password_user_without_permission(self):
+
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        self.ldb.newuser(SECOND_USER_NAME, SECOND_USER_PASS)
+
+        #
+        # Get the password reset from the user add
+        #
+        dn = "CN=" + SECOND_USER_NAME + ",CN=Users," + self.base_dn
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["passwordChange"]
+        self.assertEquals("Reset", audit["action"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "LDAP")
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+        self.assertEquals(0, audit["statusCode"])
+        self.assertEquals("Success", audit["status"])
+        self.discardMessages()
+
+        creds = self.insta_creds(
+            template=self.get_credentials(),
+            username=SECOND_USER_NAME,
+            userpass=SECOND_USER_PASS,
+            kerberos_state=None)
+
+        lp = self.get_loadparm()
+        net = Net(creds, lp, server=self.server)
+        password = "newPassword!!42"
+        domain = lp.get("workgroup")
+
+        try:
+            net.set_password(newpassword=password.encode('utf-8'),
+                             account_name=USER_NAME,
+                             domain_name=domain)
+            self.fail("Expected exception not thrown")
+        except Exception:
+            pass
+
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        messages = self.waitForMessages(1, net, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["passwordChange"]
+        self.assertEquals("Reset", audit["action"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "DCE/RPC")
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+        self.assertEquals(ERR_INSUFFICIENT_ACCESS_RIGHTS, audit["statusCode"])
+        self.assertEquals("insufficient access rights", audit["status"])
+
+    def test_net_set_password(self):
+
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        creds = self.insta_creds(template=self.get_credentials())
+
+        lp = self.get_loadparm()
+        net = Net(creds, lp, server=self.server)
+        password = "newPassword!!42"
+        domain = lp.get("workgroup")
+
+        net.set_password(newpassword=password.encode('utf-8'),
+                         account_name=USER_NAME,
+                         domain_name=domain)
+
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        messages = self.waitForMessages(1, net, dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["passwordChange"]
+        self.assertEquals("Reset", audit["action"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "DCE/RPC")
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+
+    def test_ldap_change_password(self):
+
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        new_password = samba.generate_random_password(32, 32)
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: modify\n" +
+            "delete: userPassword\n" +
+            "userPassword: " + USER_PASS + "\n" +
+            "add: userPassword\n" +
+            "userPassword: " + new_password + "\n")
+
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["passwordChange"]
+        self.assertEquals("Change", audit["action"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "LDAP")
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+
+    def test_ldap_replace_password(self):
+
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        self.discardSetupMessages(dn)
+
+        new_password = samba.generate_random_password(32, 32)
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: modify\n" +
+            "replace: userPassword\n" +
+            "userPassword: " + new_password + "\n")
+
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["passwordChange"]
+        self.assertEquals("Reset", audit["action"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "LDAP")
+        self.assertTrue(self.is_guid(audit["transactionId"]))
+
+    def test_ldap_add_user(self):
+
+        # The setup code adds a user, so we check for the password event
+        # generated by it.
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        messages = self.waitForMessages(1, dn=dn)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        #
+        # The first message should be the reset from the Setup code.
+        #
+        audit = messages[0]["passwordChange"]
+        self.assertEquals("Reset", audit["action"])
+        self.assertEquals(dn, audit["dn"])
+        self.assertRegexpMatches(audit["remoteAddress"],
+                                 self.remoteAddress)
+        session_id = self.get_session()
+        self.assertEquals(session_id, audit["sessionId"])
+        service_description = self.get_service_description()
+        self.assertEquals(service_description, "LDAP")
+        self.assertTrue(self.is_guid(audit["sessionId"]))
+        self.assertTrue(self.is_guid(audit["transactionId"]))
index 14e312f858647369491e16cda93f4e61e798b41a..3df226f80d206f9c6fdc06dfb9585679c84e5091 100755 (executable)
@@ -1524,6 +1524,8 @@ sub provision_ad_dc_ntvfs($$)
        lsa over netlogon = yes
         rpc server port = 1027
         auth event notification = true
+       dsdb event notification = true
+       dsdb password event notification = true
        server schannel = auto
        ";
        my $ret = $self->provision($prefix,
@@ -1896,6 +1898,8 @@ sub provision_ad_dc($$$$$$)
 
        server schannel = auto
         auth event notification = true
+       dsdb event notification = true
+       dsdb password event notification = true
         $smbconf_args
 ";
 
diff --git a/source4/dsdb/samdb/ldb_modules/audit_log.c b/source4/dsdb/samdb/ldb_modules/audit_log.c
new file mode 100644 (file)
index 0000000..fd23876
--- /dev/null
@@ -0,0 +1,1560 @@
+/*
+   ldb database library
+
+   Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+ * Provide an audit log of changes made to the database and at a
+ * higher level details of any password changes and resets.
+ *
+ */
+
+#include "includes.h"
+#include "ldb_module.h"
+#include "lib/audit_logging/audit_logging.h"
+
+#include "dsdb/samdb/samdb.h"
+#include "dsdb/samdb/ldb_modules/util.h"
+#include "libcli/security/dom_sid.h"
+#include "auth/common_auth.h"
+#include "param/param.h"
+
+#define OPERATION_JSON_TYPE "dsdbChange"
+#define OPERATION_HR_TAG "DSDB Change"
+#define OPERATION_MAJOR 1
+#define OPERATION_MINOR 0
+#define OPERATION_LOG_LVL 5
+
+#define PASSWORD_JSON_TYPE "passwordChange"
+#define PASSWORD_HR_TAG "Password Change"
+#define PASSWORD_MAJOR 1
+#define PASSWORD_MINOR 0
+#define PASSWORD_LOG_LVL 5
+
+#define TRANSACTION_JSON_TYPE "dsdbTransaction"
+#define TRANSACTION_HR_TAG "DSDB Transaction"
+#define TRANSACTION_MAJOR 1
+#define TRANSACTION_MINOR 0
+/*
+ * Currently we only log roll backs and prepare commit failures
+ */
+#define TRANSACTION_LOG_LVL 5
+
+#define REPLICATION_JSON_TYPE "replicatedUpdate"
+#define REPLICATION_HR_TAG "Replicated Update"
+#define REPLICATION_MAJOR 1
+#define REPLICATION_MINOR 0
+#define REPLICATION_LOG_LVL 5
+/*
+ * Attribute values are truncated in the logs if they are longer than
+ * MAX_LENGTH
+ */
+#define MAX_LENGTH 1024
+
+#define min(a, b) (((a)>(b))?(b):(a))
+
+/*
+ * Private data for the module, stored in the ldb_module private data
+ */
+struct audit_context {
+       /*
+        * Should details of database operations be sent over the
+        * messaging bus.
+        */
+       bool send_samdb_events;
+       /*
+        * Should details of password changes and resets be sent over
+        * the messaging bus.
+        */
+       bool send_password_events;
+       /*
+        * The messaging context to send the messages over.  Will only
+        * be set if send_samdb_events or send_password_events are
+        * true.
+        */
+       struct imessaging_context *msg_ctx;
+       /*
+        * Unique transaction id for the current transaction
+        */
+       struct GUID transaction_guid;
+};
+
+/*
+ * @brief Has the password changed.
+ *
+ * Does the message contain a change to one of the password attributes? The
+ * password attributes are defined in DSDB_PASSWORD_ATTRIBUTES
+ *
+ * @return true if the message contains a password attribute
+ *
+ */
+static bool has_password_changed(const struct ldb_message *message)
+{
+       int i;
+       if (message == NULL) {
+               return false;
+       }
+       for (i=0;i<message->num_elements;i++) {
+               if (dsdb_audit_is_password_attribute(
+                       message->elements[i].name)) {
+                       return true;
+               }
+       }
+       return false;
+}
+
+/*
+ * @brief Is the request a password "Change" or a "Reset"
+ *
+ * Get a description of the action being performed on the user password.  This
+ * routine assumes that the request contains password attributes and that the
+ * password ACL checks have been performed by acl.c
+ *
+ * @param request the ldb_request to inspect
+ * @param reply the ldb_reply, will contain the password controls
+ *
+ * @return "Change" if the password is being changed.
+ *         "Reset"  if the password is being reset.
+ */
+static const char *get_password_action(
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+       if(request->operation == LDB_ADD) {
+               return "Reset";
+       } else {
+               struct ldb_control *pav_ctrl = NULL;
+               struct dsdb_control_password_acl_validation *pav = NULL;
+
+               pav_ctrl = ldb_reply_get_control(
+                       discard_const(reply),
+                       DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID);
+               if (pav_ctrl == NULL) {
+                       return "Reset";
+               }
+
+               pav = talloc_get_type_abort(
+                       pav_ctrl->data,
+                       struct dsdb_control_password_acl_validation);
+
+               if (pav->pwd_reset) {
+                       return "Reset";
+               } else {
+                       return "Change";
+               }
+       }
+}
+
+
+#ifdef HAVE_JANSSON
+/*
+ * @brief generate a JSON object detailing an ldb operation.
+ *
+ * Generate a JSON object detailing an ldb operation.
+ *
+ * @param module the ldb module
+ * @param request the request
+ * @param reply the result of the operation.
+ *
+ * @return the generated JSON object, should be freed with json_free.
+ *
+ */
+static struct json_object operation_json(
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+       struct ldb_context *ldb = NULL;
+       const struct dom_sid *sid = NULL;
+       bool as_system = false;
+       struct json_object wrapper;
+       struct json_object audit;
+       const struct tsocket_address *remote = NULL;
+       const char *dn = NULL;
+       const char* operation = NULL;
+       const struct GUID *unique_session_token = NULL;
+       const struct ldb_message *message = NULL;
+       struct audit_context *ac = talloc_get_type(
+               ldb_module_get_private(module),
+               struct audit_context);
+
+       ldb = ldb_module_get_ctx(module);
+
+       remote = dsdb_audit_get_remote_address(ldb);
+       if (remote != NULL && dsdb_audit_is_system_session(module)) {
+               as_system = true;
+               sid = dsdb_audit_get_actual_sid(ldb);
+               unique_session_token =
+                       dsdb_audit_get_actual_unique_session_token(ldb);
+       } else {
+               sid = dsdb_audit_get_user_sid(module);
+               unique_session_token =
+                       dsdb_audit_get_unique_session_token(module);
+       }
+       dn = dsdb_audit_get_primary_dn(request);
+       operation = dsdb_audit_get_operation_name(request);
+
+       audit = json_new_object();
+       json_add_version(&audit, OPERATION_MAJOR, OPERATION_MINOR);
+       json_add_int(&audit, "statusCode", reply->error);
+       json_add_string(&audit, "status", ldb_strerror(reply->error));
+       json_add_string(&audit, "operation", operation);
+       json_add_address(&audit, "remoteAddress", remote);
+       json_add_bool(&audit, "performedAsSystem", as_system);
+       json_add_sid(&audit, "userSid", sid);
+       json_add_string(&audit, "dn", dn);
+       json_add_guid(&audit, "transactionId", &ac->transaction_guid);
+       json_add_guid(&audit, "sessionId", unique_session_token);
+
+       message = dsdb_audit_get_message(request);
+       if (message != NULL) {
+               struct json_object attributes =
+                       dsdb_audit_attributes_json(
+                               request->operation,
+                               message);
+               json_add_object(&audit, "attributes", &attributes);
+       }
+
+       wrapper = json_new_object();
+       json_add_timestamp(&wrapper);
+       json_add_string(&wrapper, "type", OPERATION_JSON_TYPE);
+       json_add_object(&wrapper, OPERATION_JSON_TYPE, &audit);
+       return wrapper;
+}
+
+/*
+ * @brief generate a JSON object detailing a replicated update.
+ *
+ * Generate a JSON object detailing a replicated update
+ *
+ * @param module the ldb module
+ * @param request the request
+ * @paran reply the result of the operation
+ *
+ * @return the generated JSON object, should be freed with json_free.
+ *
+ */
+static struct json_object replicated_update_json(
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+       struct json_object wrapper;
+       struct json_object audit;
+       struct audit_context *ac = talloc_get_type(
+               ldb_module_get_private(module),
+               struct audit_context);
+       struct dsdb_extended_replicated_objects *ro = talloc_get_type(
+               request->op.extended.data,
+               struct dsdb_extended_replicated_objects);
+       const char *partition_dn = NULL;
+       const char *error = NULL;
+
+       partition_dn = ldb_dn_get_linearized(ro->partition_dn);
+       error = get_friendly_werror_msg(ro->error);
+
+       audit = json_new_object();
+       json_add_version(&audit, REPLICATION_MAJOR, REPLICATION_MINOR);
+       json_add_int(&audit, "statusCode", reply->error);
+       json_add_string(&audit, "status", ldb_strerror(reply->error));
+       json_add_guid(&audit, "transactionId", &ac->transaction_guid);
+       json_add_int(&audit, "objectCount", ro->num_objects);
+       json_add_int(&audit, "linkCount", ro->linked_attributes_count);
+       json_add_string(&audit, "partitionDN", partition_dn);
+       json_add_string(&audit, "error", error);
+       json_add_int(&audit, "errorCode", W_ERROR_V(ro->error));
+       json_add_guid(
+               &audit,
+               "sourceDsa",
+               &ro->source_dsa->source_dsa_obj_guid);
+       json_add_guid(
+               &audit,
+               "invocationId",
+               &ro->source_dsa->source_dsa_invocation_id);
+
+       wrapper = json_new_object();
+       json_add_timestamp(&wrapper);
+       json_add_string(&wrapper, "type", REPLICATION_JSON_TYPE);
+       json_add_object(&wrapper, REPLICATION_JSON_TYPE, &audit);
+       return wrapper;
+}
+
+/*
+ * @brief generate a JSON object detailing a password change.
+ *
+ * Generate a JSON object detailing a password change.
+ *
+ * @param module the ldb module
+ * @param request the request
+ * @param reply the result/response
+ * @param status the status code returned for the underlying ldb operation.
+ *
+ * @return the generated JSON object.
+ *
+ */
+static struct json_object password_change_json(
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+       struct ldb_context *ldb = NULL;
+       const struct dom_sid *sid = NULL;
+       const char* dn = NULL;
+       struct json_object wrapper;
+       struct json_object audit;
+       const struct tsocket_address *remote = NULL;
+       const char* action = NULL;
+       const struct GUID *unique_session_token = NULL;
+       struct audit_context *ac = talloc_get_type(
+               ldb_module_get_private(module),
+               struct audit_context);
+
+
+       ldb = ldb_module_get_ctx(module);
+
+       remote = dsdb_audit_get_remote_address(ldb);
+       sid = dsdb_audit_get_user_sid(module);
+       dn = dsdb_audit_get_primary_dn(request);
+       action = get_password_action(request, reply);
+       unique_session_token = dsdb_audit_get_unique_session_token(module);
+
+       audit = json_new_object();
+       json_add_version(&audit, PASSWORD_MAJOR, PASSWORD_MINOR);
+       json_add_int(&audit, "statusCode", reply->error);
+       json_add_string(&audit, "status", ldb_strerror(reply->error));
+       json_add_address(&audit, "remoteAddress", remote);
+       json_add_sid(&audit, "userSid", sid);
+       json_add_string(&audit, "dn", dn);
+       json_add_string(&audit, "action", action);
+       json_add_guid(&audit, "transactionId", &ac->transaction_guid);
+       json_add_guid(&audit, "sessionId", unique_session_token);
+
+       wrapper = json_new_object();
+       json_add_timestamp(&wrapper);
+       json_add_string(&wrapper, "type", PASSWORD_JSON_TYPE);
+       json_add_object(&wrapper, PASSWORD_JSON_TYPE, &audit);
+
+       return wrapper;
+}
+
+
+/*
+ * @brief create a JSON object containing details of a transaction event.
+ *
+ * Create a JSON object detailing a transaction transaction life cycle events,
+ * i.e. begin, commit, roll back
+ *
+ * @param action a one word description of the event/action
+ * @param transaction_id the GUID identifying the current transaction.
+ *
+ * @return a JSON object detailing the event
+ */
+static struct json_object transaction_json(
+       const char *action,
+       struct GUID *transaction_id)
+{
+       struct json_object wrapper;
+       struct json_object audit;
+
+       audit = json_new_object();
+       json_add_version(&audit, TRANSACTION_MAJOR, TRANSACTION_MINOR);
+       json_add_string(&audit, "action", action);
+       json_add_guid(&audit, "transactionId", transaction_id);
+
+       wrapper = json_new_object();
+       json_add_timestamp(&wrapper);
+       json_add_string(&wrapper, "type", TRANSACTION_JSON_TYPE);
+       json_add_object(&wrapper, TRANSACTION_JSON_TYPE, &audit);
+
+       return wrapper;
+}
+
+
+/*
+ * @brief generate a JSON object detailing a commit failure.
+ *
+ * Generate a JSON object containing details of a commit failure.
+ *
+ * @param action the commit action, "commit" or "prepare"
+ * @param status the status code returned by commit
+ * @param reason any extra failure information/reason available
+ * @param transaction_id the GUID identifying the current transaction.
+ */
+static struct json_object commit_failure_json(
+       const char *action,
+       int status,
+       const char *reason,
+       struct GUID *transaction_id)
+{
+       struct json_object wrapper;
+       struct json_object audit;
+
+       audit = json_new_object();
+       json_add_version(&audit, TRANSACTION_MAJOR, TRANSACTION_MINOR);
+       json_add_string(&audit, "action", action);
+       json_add_guid(&audit, "transactionId", transaction_id);
+       json_add_int(&audit, "statusCode", status);
+       json_add_string(&audit, "status", ldb_strerror(status));
+       json_add_string(&audit, "reason", reason);
+
+       wrapper = json_new_object();
+       json_add_timestamp(&wrapper);
+       json_add_string(&wrapper, "type", TRANSACTION_JSON_TYPE);
+       json_add_object(&wrapper, TRANSACTION_JSON_TYPE, &audit);
+
+       return wrapper;
+}
+
+#endif
+/*
+ * @brief Print a human readable log line for a password change event.
+ *
+ * Generate a human readable log line detailing a password change.
+ *
+ * @param mem_ctx The talloc context that will own the generated log line.
+ * @param module the ldb module
+ * @param request the request
+ * @param reply the result/response
+ * @param status the status code returned for the underlying ldb operation.
+ *
+ * @return the generated log line.
+ */
+static char *password_change_human_readable(
+       TALLOC_CTX *mem_ctx,
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+       struct ldb_context *ldb = NULL;
+       const char *remote_host = NULL;
+       const struct dom_sid *sid = NULL;
+       const char *user_sid = NULL;
+       const char *timestamp = NULL;
+       char *log_entry = NULL;
+       const char *action = NULL;
+       const char *dn = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = ldb_module_get_ctx(module);
+
+       remote_host = dsdb_audit_get_remote_host(ldb, ctx);
+       sid = dsdb_audit_get_user_sid(module);
+       user_sid = dom_sid_string(ctx, sid);
+       timestamp = audit_get_timestamp(ctx);
+       action = get_password_action(request, reply);
+       dn = dsdb_audit_get_primary_dn(request);
+
+       log_entry = talloc_asprintf(
+               mem_ctx,
+               "[%s] at [%s] status [%s] "
+               "remote host [%s] SID [%s] DN [%s]",
+               action,
+               timestamp,
+               ldb_strerror(reply->error),
+               remote_host,
+               user_sid,
+               dn);
+       TALLOC_FREE(ctx);
+       return log_entry;
+}
+/*
+ * @brief Generate a human readable string, detailing attributes in a message
+ *
+ * For modify operations each attribute is prefixed with the action.
+ * Normal values are enclosed in []
+ * Base64 values are enclosed in {}
+ * Truncated values are indicated by three trailing dots "..."
+ *
+ * @param ldb The ldb_context
+ * @param buffer The attributes will be appended to the buffer.
+ *               assumed to have been allocated via talloc.
+ * @param operation The operation type
+ * @param message the message to process
+ *
+ */
+static char *log_attributes(
+       struct ldb_context *ldb,
+       char *buffer,
+       enum ldb_request_type operation,
+       const struct ldb_message *message)
+{
+       int i, j;
+       for (i=0;i<message->num_elements;i++) {
+               if (i > 0) {
+                       buffer = talloc_asprintf_append_buffer(buffer, " ");
+               }
+
+               if (message->elements[i].name == NULL) {
+                       ldb_debug(
+                               ldb,
+                               LDB_DEBUG_ERROR,
+                               "Error: Invalid element name (NULL) at "
+                               "position %d", i);
+                       return NULL;
+               }
+
+               if (operation == LDB_MODIFY) {
+                       const char *action =NULL;
+                       action = dsdb_audit_get_modification_action(
+                               message->elements[i].flags);
+                       buffer = talloc_asprintf_append_buffer(
+                               buffer,
+                               "%s: %s ",
+                               action,
+                               message->elements[i].name);
+               } else {
+                       buffer = talloc_asprintf_append_buffer(
+                               buffer,
+                               "%s ",
+                               message->elements[i].name);
+               }
+
+               if (dsdb_audit_redact_attribute(message->elements[i].name)) {
+                       /*
+                        * Do not log the value of any secret or password
+                        * attributes
+                        */
+                       buffer = talloc_asprintf_append_buffer(
+                               buffer,
+                               "[REDACTED SECRET ATTRIBUTE]");
+                       continue;
+               }
+
+               for (j=0;j<message->elements[i].num_values;j++) {
+                       struct ldb_val v;
+                       bool use_b64_encode = false;
+                       int length;
+                       if (j > 0) {
+                               buffer = talloc_asprintf_append_buffer(
+                                       buffer,
+                                       " ");
+                       }
+
+                       v = message->elements[i].values[j];
+                       length = min(MAX_LENGTH, v.length);
+                       use_b64_encode = ldb_should_b64_encode(ldb, &v);
+                       if (use_b64_encode) {
+                               const char *encoded = ldb_base64_encode(
+                                       buffer,
+                                       (char *)v.data,
+                                       length);
+                               buffer = talloc_asprintf_append_buffer(
+                                       buffer,
+                                       "{%s%s}",
+                                       encoded,
+                                       (v.length > MAX_LENGTH ? "..." : ""));
+                       } else {
+                               buffer = talloc_asprintf_append_buffer(
+                                       buffer,
+                                       "[%*.*s%s]",
+                                       length,
+                                       length,
+                                       (char *)v.data,
+                                       (v.length > MAX_LENGTH ? "..." : ""));
+                       }
+               }
+       }
+       return buffer;
+}
+
+/*
+ * @brief generate a human readable log entry detailing an ldb operation.
+ *
+ * Generate a human readable log entry detailing an ldb operation.
+ *
+ * @param mem_ctx The talloc context owning the returned string.
+ * @param module the ldb module
+ * @param request the request
+ * @param reply the result of the operation
+ *
+ * @return the log entry.
+ *
+ */
+static char *operation_human_readable(
+       TALLOC_CTX *mem_ctx,
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+       struct ldb_context *ldb = NULL;
+       const char *remote_host = NULL;
+       const struct dom_sid *sid = NULL;
+       const char *user_sid = NULL;
+       const char *timestamp = NULL;
+       const char *op_name = NULL;
+       char *log_entry = NULL;
+       const char *dn = NULL;
+       const char *new_dn = NULL;
+       const struct ldb_message *message = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = ldb_module_get_ctx(module);
+
+       remote_host = dsdb_audit_get_remote_host(ldb, ctx);
+       if (remote_host != NULL && dsdb_audit_is_system_session(module)) {
+               sid = dsdb_audit_get_actual_sid(ldb);
+       } else {
+               sid = dsdb_audit_get_user_sid(module);
+       }
+       user_sid = dom_sid_string(ctx, sid);
+       timestamp = audit_get_timestamp(ctx);
+       op_name = dsdb_audit_get_operation_name(request);
+       dn = dsdb_audit_get_primary_dn(request);
+       new_dn = dsdb_audit_get_secondary_dn(request);
+
+       message = dsdb_audit_get_message(request);
+
+       log_entry = talloc_asprintf(
+               mem_ctx,
+               "[%s] at [%s] status [%s] "
+               "remote host [%s] SID [%s] DN [%s]",
+               op_name,
+               timestamp,
+               ldb_strerror(reply->error),
+               remote_host,
+               user_sid,
+               dn);
+       if (new_dn != NULL) {
+               log_entry = talloc_asprintf_append_buffer(
+                       log_entry,
+                       " New DN [%s]",
+                       new_dn);
+       }
+       if (message != NULL) {
+               log_entry = talloc_asprintf_append_buffer(log_entry,
+                                                         " attributes [");
+               log_entry = log_attributes(ldb,
+                                          log_entry,
+                                          request->operation,
+                                          message);
+               log_entry = talloc_asprintf_append_buffer(log_entry, "]");
+       }
+       TALLOC_FREE(ctx);
+       return log_entry;
+}
+
+/*
+ * @brief generate a human readable log entry detailing a replicated update
+ *        operation.
+ *
+ * Generate a human readable log entry detailing a replicated update operation
+ *
+ * @param mem_ctx The talloc context owning the returned string.
+ * @param module the ldb module
+ * @param request the request
+ * @param reply the result of the operation.
+ *
+ * @return the log entry.
+ *
+ */
+static char *replicated_update_human_readable(
+       TALLOC_CTX *mem_ctx,
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+       struct dsdb_extended_replicated_objects *ro = talloc_get_type(
+               request->op.extended.data,
+               struct dsdb_extended_replicated_objects);
+       const char *partition_dn = NULL;
+       const char *error = NULL;
+       char *log_entry = NULL;
+       char *timestamp = NULL;
+       struct GUID_txt_buf object_buf;
+       const char *object = NULL;
+       struct GUID_txt_buf invocation_buf;
+       const char *invocation = NULL;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       timestamp = audit_get_timestamp(ctx);
+       error = get_friendly_werror_msg(ro->error);
+       partition_dn = ldb_dn_get_linearized(ro->partition_dn);
+       object = GUID_buf_string(
+               &ro->source_dsa->source_dsa_obj_guid,
+               &object_buf);
+       invocation = GUID_buf_string(
+               &ro->source_dsa->source_dsa_invocation_id,
+               &invocation_buf);
+
+
+       log_entry = talloc_asprintf(
+               mem_ctx,
+               "at [%s] status [%s] error [%s] partition [%s] objects [%d] "
+               "links [%d] object [%s] invocation [%s]",
+               timestamp,
+               ldb_strerror(reply->error),
+               error,
+               partition_dn,
+               ro->num_objects,
+               ro->linked_attributes_count,
+               object,
+               invocation);
+
+       TALLOC_FREE(ctx);
+       return log_entry;
+}
+/*
+ * @brief create a human readable log entry detailing a transaction event.
+ *
+ * Create a human readable log entry detailing a transaction event.
+ * i.e. begin, commit, roll back
+ *
+ * @param mem_ctx The talloc context owning the returned string.
+ * @param action a one word description of the event/action
+ * @param transaction_id the GUID identifying the current transaction.
+ *
+ * @return the log entry
+ */
+static char *transaction_human_readable(
+       TALLOC_CTX *mem_ctx,
+       const char* action)
+{
+       const char *timestamp = NULL;
+       char *log_entry = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       timestamp = audit_get_timestamp(ctx);
+
+       log_entry = talloc_asprintf(
+               mem_ctx,
+               "[%s] at [%s]",
+               action,
+               timestamp);
+
+       TALLOC_FREE(ctx);
+       return log_entry;
+}
+
+/*
+ * @brief generate a human readable log entry detailing a commit failure.
+ *
+ * Generate generate a human readable log entry detailing a commit failure.
+ *
+ * @param mem_ctx The talloc context owning the returned string.
+ * @param action the commit action, "prepare" or "commit"
+ * @param status the status code returned by commit
+ * @param reason any extra failure information/reason available
+ *
+ * @return the log entry
+ */
+static char *commit_failure_human_readable(
+       TALLOC_CTX *mem_ctx,
+       const char *action,
+       int status,
+       const char *reason)
+{
+       const char *timestamp = NULL;
+       char *log_entry = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       timestamp = audit_get_timestamp(ctx);
+
+       log_entry = talloc_asprintf(
+               mem_ctx,
+               "[%s] at [%s] status [%d] reason [%s]",
+               action,
+               timestamp,
+               status,
+               reason);
+
+       TALLOC_FREE(ctx);
+       return log_entry;
+}
+
+/*
+ * @brief log details of a standard ldb operation.
+ *
+ * Log the details of an ldb operation in JSON and or human readable format
+ * and send over the message bus.
+ *
+ * @param module the ldb_module
+ * @param request the operation request.
+ * @param reply the operation result.
+ * @param the status code returned for the operation.
+ *
+ */
+static void log_standard_operation(
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+
+       const struct ldb_message *message = dsdb_audit_get_message(request);
+       bool password_changed = has_password_changed(message);
+       struct audit_context *ac =
+               talloc_get_type(ldb_module_get_private(module),
+                               struct audit_context);
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_AUDIT, OPERATION_LOG_LVL)) {
+               char *entry = NULL;
+               entry = operation_human_readable(
+                       ctx,
+                       module,
+                       request,
+                       reply);
+               audit_log_human_text(
+                       OPERATION_HR_TAG,
+                       entry,
+                       DBGC_DSDB_AUDIT,
+                       OPERATION_LOG_LVL);
+               TALLOC_FREE(entry);
+       }
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_PWD_AUDIT, PASSWORD_LOG_LVL)) {
+               if (password_changed) {
+                       char *entry = NULL;
+                       entry = password_change_human_readable(
+                               ctx,
+                               module,
+                               request,
+                               reply);
+                       audit_log_human_text(
+                               PASSWORD_HR_TAG,
+                               entry,
+                               DBGC_DSDB_PWD_AUDIT,
+                               PASSWORD_LOG_LVL);
+                       TALLOC_FREE(entry);
+               }
+       }
+#ifdef HAVE_JANSSON
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_AUDIT_JSON, OPERATION_LOG_LVL) ||
+               (ac->msg_ctx && ac->send_samdb_events)) {
+               struct json_object json;
+               json = operation_json(module, request, reply);
+               audit_log_json(
+                       OPERATION_JSON_TYPE,
+                       &json,
+                       DBGC_DSDB_AUDIT_JSON,
+                       OPERATION_LOG_LVL);
+               if (ac->msg_ctx && ac->send_password_events) {
+                       audit_message_send(
+                               ac->msg_ctx,
+                               DSDB_EVENT_NAME,
+                               MSG_DSDB_LOG,
+                               &json);
+               }
+               json_free(&json);
+       }
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_PWD_AUDIT_JSON, PASSWORD_LOG_LVL) ||
+               (ac->msg_ctx && ac->send_password_events)) {
+               if (password_changed) {
+                       struct json_object json;
+                       json = password_change_json(module, request, reply);
+                       audit_log_json(
+                               PASSWORD_JSON_TYPE,
+                               &json,
+                               DBGC_DSDB_PWD_AUDIT_JSON,
+                               PASSWORD_LOG_LVL);
+                       if (ac->send_password_events) {
+                               audit_message_send(
+                                       ac->msg_ctx,
+                                       DSDB_PWD_EVENT_NAME,
+                                       MSG_DSDB_PWD_LOG,
+                                       &json);
+                       }
+                       json_free(&json);
+               }
+       }
+#endif
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * @brief log details of a replicated update.
+ *
+ * Log the details of a replicated update in JSON and or human readable
+ * format and send over the message bus.
+ *
+ * @param module the ldb_module
+ * @param request the operation request
+ * @param reply the result of the operation.
+ *
+ */
+static void log_replicated_operation(
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+
+       struct audit_context *ac =
+               talloc_get_type(ldb_module_get_private(module),
+                               struct audit_context);
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_AUDIT, REPLICATION_LOG_LVL)) {
+               char *entry = NULL;
+               entry = replicated_update_human_readable(
+                       ctx,
+                       module,
+                       request,
+                       reply);
+               audit_log_human_text(
+                       REPLICATION_HR_TAG,
+                       entry,
+                       DBGC_DSDB_AUDIT,
+                       REPLICATION_LOG_LVL);
+               TALLOC_FREE(entry);
+       }
+#ifdef HAVE_JANSSON
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_AUDIT_JSON, REPLICATION_LOG_LVL) ||
+               (ac->msg_ctx && ac->send_samdb_events)) {
+               struct json_object json;
+               json = replicated_update_json(module, request, reply);
+               audit_log_json(
+                       REPLICATION_JSON_TYPE,
+                       &json,
+                       DBGC_DSDB_AUDIT_JSON,
+                       REPLICATION_LOG_LVL);
+               if (ac->send_samdb_events) {
+                       audit_message_send(
+                               ac->msg_ctx,
+                               DSDB_EVENT_NAME,
+                               MSG_DSDB_LOG,
+                               &json);
+               }
+               json_free(&json);
+       }
+#endif
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * @brief log details of an ldb operation.
+ *
+ * Log the details of an ldb operation in JSON and or human readable format
+ * and send over the message bus.
+ *
+ * @param module the ldb_module
+ * @param request the operation request
+ * @part reply the result of the operation
+ *
+ */
+static void log_operation(
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const struct ldb_reply *reply)
+{
+
+       if (request->operation == LDB_EXTENDED) {
+               if (strcmp(
+                       request->op.extended.oid,
+                       DSDB_EXTENDED_REPLICATED_OBJECTS_OID) != 0) {
+
+                       log_replicated_operation(module, request, reply);
+               }
+       } else {
+               log_standard_operation(module, request, reply);
+       }
+}
+
+/*
+ * @brief log details of a transaction event.
+ *
+ * Log the details of a transaction event in JSON and or human readable format
+ * and send over the message bus.
+ *
+ * @param module the ldb_module
+ * @param  action the transaction event i.e. begin, commit, roll back.
+ *
+ */
+static void log_transaction(
+       struct ldb_module *module,
+       const char *action)
+{
+
+       struct audit_context *ac =
+               talloc_get_type(ldb_module_get_private(module),
+                               struct audit_context);
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_TXN_AUDIT, TRANSACTION_LOG_LVL)) {
+               char* entry = NULL;
+               entry = transaction_human_readable(ctx, action);
+               audit_log_human_text(
+                       TRANSACTION_HR_TAG,
+                       entry,
+                       DBGC_DSDB_TXN_AUDIT,
+                       TRANSACTION_LOG_LVL);
+               TALLOC_FREE(entry);
+       }
+#ifdef HAVE_JANSSON
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_TXN_AUDIT_JSON, TRANSACTION_LOG_LVL) ||
+               (ac->msg_ctx && ac->send_samdb_events)) {
+               struct json_object json;
+               json = transaction_json(action, &ac->transaction_guid);
+               audit_log_json(
+                       TRANSACTION_JSON_TYPE,
+                       &json,
+                       DBGC_DSDB_TXN_AUDIT_JSON,
+                       TRANSACTION_LOG_LVL);
+               if (ac->send_samdb_events) {
+                       audit_message_send(
+                               ac->msg_ctx,
+                               DSDB_EVENT_NAME,
+                               MSG_DSDB_LOG,
+                               &json);
+               }
+               json_free(&json);
+       }
+#endif
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * @brief log details of a commit failure.
+ *
+ * Log the details of a commit failure in JSON and or human readable
+ * format and send over the message bus.
+ *
+ * @param module the ldb_module
+ * @param action the commit action "prepare" or "commit"
+ * @param status the ldb status code returned by prepare commit.
+ *
+ */
+static void log_commit_failure(
+       struct ldb_module *module,
+       const char *action,
+       int status)
+{
+
+       struct audit_context *ac =
+               talloc_get_type(ldb_module_get_private(module),
+                               struct audit_context);
+       const char* reason = dsdb_audit_get_ldb_error_string(module, status);
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_TXN_AUDIT, TRANSACTION_LOG_LVL)) {
+               char* entry = NULL;
+               entry = commit_failure_human_readable(
+                       ctx,
+                       action,
+                       status,
+                       reason);
+               audit_log_human_text(
+                       TRANSACTION_HR_TAG,
+                       entry,
+                       DBGC_DSDB_TXN_AUDIT,
+                       TRANSACTION_LOG_LVL);
+               TALLOC_FREE(entry);
+       }
+#ifdef HAVE_JANSSON
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_TXN_AUDIT_JSON, TRANSACTION_LOG_LVL) ||
+               (ac->msg_ctx && ac->send_samdb_events)) {
+               struct json_object json;
+               json = commit_failure_json(
+                       action,
+                       status,
+                       reason,
+                       &ac->transaction_guid);
+               audit_log_json(
+                       TRANSACTION_JSON_TYPE,
+                       &json,
+                       DBGC_DSDB_TXN_AUDIT_JSON,
+                       TRANSACTION_LOG_LVL);
+               if (ac->send_samdb_events) {
+                       audit_message_send(ac->msg_ctx,
+                                          DSDB_EVENT_NAME,
+                                          MSG_DSDB_LOG,
+                                          &json);
+               }
+               json_free(&json);
+       }
+#endif
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * Context needed by audit_callback
+ */
+struct audit_callback_context {
+       struct ldb_request *request;
+       struct ldb_module *module;
+};
+
+/*
+ * @brief call back function for the ldb_operations.
+ *
+ * As the LDB operations are async, and we wish to examine the results of
+ * the operations, a callback needs to be registered to process the results
+ * of the LDB operations.
+ *
+ * @param req the ldb request
+ * @param res the result of the operation
+ *
+ * @return the LDB_STATUS
+ */
+static int audit_callback(struct ldb_request *req, struct ldb_reply *ares)
+{
+       struct audit_callback_context *ac = NULL;
+
+       ac = talloc_get_type(
+               req->context,
+               struct audit_callback_context);
+
+       if (!ares) {
+               return ldb_module_done(
+                       ac->request,
+                       NULL,
+                       NULL,
+                       LDB_ERR_OPERATIONS_ERROR);
+       }
+
+       /* pass on to the callback */
+       switch (ares->type) {
+       case LDB_REPLY_ENTRY:
+               return ldb_module_send_entry(
+                       ac->request,
+                       ares->message,
+                       ares->controls);
+
+       case LDB_REPLY_REFERRAL:
+               return ldb_module_send_referral(
+                       ac->request,
+                       ares->referral);
+
+       case LDB_REPLY_DONE:
+               /*
+                * Log the operation once DONE
+                */
+               log_operation(ac->module, ac->request, ares);
+               return ldb_module_done(
+                       ac->request,
+                       ares->controls,
+                       ares->response,
+                       ares->error);
+
+       default:
+               /* Can't happen */
+               return LDB_ERR_OPERATIONS_ERROR;
+       }
+}
+
+/*
+ * @brief Add the current transaction identifier to the request.
+ *
+ * Add the current transaction identifier in the module private data,
+ * to the request as a control.
+ *
+ * @param module
+ * @param req the request.
+ *
+ * @return an LDB_STATUS code, LDB_SUCCESS if successful.
+ */
+static int add_transaction_id(
+       struct ldb_module *module,
+       struct ldb_request *req)
+{
+       struct audit_context *ac =
+               talloc_get_type(ldb_module_get_private(module),
+                               struct audit_context);
+       struct dsdb_control_transaction_identifier *transaction_id;
+
+       transaction_id = talloc_zero(
+               req,
+               struct dsdb_control_transaction_identifier);
+       if (transaction_id == NULL) {
+               struct ldb_context *ldb = ldb_module_get_ctx(module);
+               return ldb_oom(ldb);
+       }
+       transaction_id->transaction_guid = ac->transaction_guid;
+       ldb_request_add_control(req,
+                               DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID,
+                               false,
+                               transaction_id);
+       return LDB_SUCCESS;
+
+}
+
+/*
+ * @brief log details of an add operation.
+ *
+ * Log the details of an add operation.
+ *
+ * @param module the ldb_module
+ * @param req the ldb_request
+ *
+ * @return ldb status code
+ */
+static int log_add(
+       struct ldb_module *module,
+       struct ldb_request *req)
+{
+       struct audit_callback_context *context = NULL;
+       struct ldb_request *new_req = NULL;
+       struct ldb_context *ldb = NULL;
+       int ret;
+
+       ldb = ldb_module_get_ctx(module);
+       context = talloc_zero(req, struct audit_callback_context);
+
+       if (context == NULL) {
+               return ldb_oom(ldb);
+       }
+       context->request = req;
+       context->module  = module;
+       /*
+        * We want to log the return code status, so we need to register
+        * a callback function to get the actual result.
+        * We need to take a new copy so that we don't alter the callers copy
+        */
+       ret = ldb_build_add_req(
+               &new_req,
+               ldb,
+               req,
+               req->op.add.message,
+               req->controls,
+               context,
+               audit_callback,
+               req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       ret = add_transaction_id(module, new_req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       return ldb_next_request(module, new_req);
+}
+
+/*
+ * @brief log details of an delete operation.
+ *
+ * Log the details of an delete operation.
+ *
+ * @param module the ldb_module
+ * @param req the ldb_request
+ *
+ * @return ldb status code
+ */
+static int log_delete(
+       struct ldb_module *module,
+       struct ldb_request *req)
+{
+       struct audit_callback_context *context = NULL;
+       struct ldb_request *new_req = NULL;
+       struct ldb_context *ldb = NULL;
+       int ret;
+
+       ldb = ldb_module_get_ctx(module);
+       context = talloc_zero(req, struct audit_callback_context);
+
+       if (context == NULL) {
+               return ldb_oom(ldb);
+       }
+       context->request = req;
+       context->module  = module;
+       /*
+        * We want to log the return code status, so we need to register
+        * a callback function to get the actual result.
+        * We need to take a new copy so that we don't alter the callers copy
+        */
+       ret = ldb_build_del_req(&new_req,
+                               ldb,
+                               req,
+                               req->op.del.dn,
+                               req->controls,
+                               context,
+                               audit_callback,
+                               req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       ret = add_transaction_id(module, new_req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       return ldb_next_request(module, new_req);
+}
+
+/*
+ * @brief log details of a modify operation.
+ *
+ * Log the details of a modify operation.
+ *
+ * @param module the ldb_module
+ * @param req the ldb_request
+ *
+ * @return ldb status code
+ */
+static int log_modify(
+       struct ldb_module *module,
+       struct ldb_request *req)
+{
+       struct audit_callback_context *context = NULL;
+       struct ldb_request *new_req = NULL;
+       struct ldb_context *ldb = NULL;
+       int ret;
+
+       ldb = ldb_module_get_ctx(module);
+       context = talloc_zero(req, struct audit_callback_context);
+
+       if (context == NULL) {
+               return ldb_oom(ldb);
+       }
+       context->request = req;
+       context->module  = module;
+       /*
+        * We want to log the return code status, so we need to register
+        * a callback function to get the actual result.
+        * We need to take a new copy so that we don't alter the callers copy
+        */
+       ret = ldb_build_mod_req(
+               & new_req,
+               ldb,
+               req,
+               req->op.mod.message,
+               req->controls,
+               context,
+               audit_callback,
+               req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       ret = add_transaction_id(module, new_req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       return ldb_next_request(module, new_req);
+}
+
+/*
+ * @brief process a transaction start.
+ *
+ * process a transaction start, as we don't currently log transaction starts
+ * just generate the new transaction_id.
+ *
+ * @param module the ldb_module
+ * @param req the ldb_request
+ *
+ * @return ldb status code
+ */
+static int log_start_transaction(struct ldb_module *module)
+{
+       struct audit_context *ac =
+               talloc_get_type(ldb_module_get_private(module),
+                               struct audit_context);
+
+       /*
+        * We do not log transaction begins
+        * however we do generate a new transaction_id
+        *
+        */
+       ac->transaction_guid = GUID_random();
+       return ldb_next_start_trans(module);
+}
+
+/*
+ * @brief log details of a prepare commit.
+ *
+ * Log the details of a prepare commit, currently only details of
+ * failures are logged.
+ *
+ * @param module the ldb_module
+ * @param req the ldb_request
+ *
+ * @return ldb status code
+ */
+static int log_prepare_commit(struct ldb_module *module)
+{
+
+       int ret = ldb_next_prepare_commit(module);
+       if (ret != LDB_SUCCESS) {
+               /*
+                * We currently only log prepare commit failures
+                */
+               log_commit_failure(module, "prepare", ret);
+       }
+       return ret;
+}
+
+/*
+ * @brief process a transaction end aka commit.
+ *
+ * process a transaction end, as we don't currently log transaction ends
+ * just clear transaction_id.
+ *
+ * @param module the ldb_module
+ * @param req the ldb_request
+ *
+ * @return ldb status code
+ */
+static int log_end_transaction(struct ldb_module *module)
+{
+       struct audit_context *ac =
+               talloc_get_type(ldb_module_get_private(module),
+                               struct audit_context);
+       int ret = 0;
+
+       /*
+        * Clear the transaction id inserted by log_start_transaction
+        */
+       memset(&ac->transaction_guid, 0, sizeof(struct GUID));
+
+       ret = ldb_next_end_trans(module);
+       if (ret != LDB_SUCCESS) {
+               /*
+                * We currently only log commit failures
+                */
+               log_commit_failure(module, "commit", ret);
+       }
+       return ret;
+}
+
+/*
+ * @brief log details of a transaction delete aka roll back.
+ *
+ * Log details of a transaction roll back.
+ *
+ * @param module the ldb_module
+ * @param req the ldb_request
+ *
+ * @return ldb status code
+ */
+static int log_del_transaction(struct ldb_module *module)
+{
+       struct audit_context *ac =
+               talloc_get_type(ldb_module_get_private(module),
+                               struct audit_context);
+
+       log_transaction(module, "rollback");
+       memset(&ac->transaction_guid, 0, sizeof(struct GUID));
+       return ldb_next_del_trans(module);
+}
+
+/*
+ * @brief log details of an extended operation.
+ *
+ * Log the details of an extended operation.
+ *
+ * @param module the ldb_module
+ * @param req the ldb_request
+ *
+ * @return ldb status code
+ */
+static int log_extended(
+       struct ldb_module *module,
+       struct ldb_request *req)
+{
+       struct audit_callback_context *context = NULL;
+       struct ldb_request *new_req = NULL;
+       struct ldb_context *ldb = NULL;
+       int ret;
+
+       /*
+        * Currently we only log replication extended operations
+        */
+       if (strcmp(
+               req->op.extended.oid,
+               DSDB_EXTENDED_REPLICATED_OBJECTS_OID) != 0) {
+
+               return ldb_next_request(module, req);
+       }
+       ldb = ldb_module_get_ctx(module);
+       context = talloc_zero(req, struct audit_callback_context);
+
+       if (context == NULL) {
+               return ldb_oom(ldb);
+       }
+       context->request = req;
+       context->module  = module;
+       /*
+        * We want to log the return code status, so we need to register
+        * a callback function to get the actual result.
+        * We need to take a new copy so that we don't alter the callers copy
+        */
+       ret = ldb_build_extended_req(
+               &new_req,
+               ldb,
+               req,
+               req->op.extended.oid,
+               req->op.extended.data,
+               req->controls,
+               context,
+               audit_callback,
+               req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       ret = add_transaction_id(module, new_req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       return ldb_next_request(module, new_req);
+}
+
+/*
+ * @brief module initialisation
+ */
+static int log_init(struct ldb_module *module)
+{
+
+       struct ldb_context *ldb = ldb_module_get_ctx(module);
+       struct audit_context *context = NULL;
+       struct loadparm_context *lp_ctx
+               = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"),
+                                       struct loadparm_context);
+       struct tevent_context *ec = ldb_get_event_context(ldb);
+       bool sdb_events = false;
+       bool pwd_events = false;
+
+       context = talloc_zero(module, struct audit_context);
+       if (context == NULL) {
+               return ldb_module_oom(module);
+       }
+
+       if (lp_ctx != NULL) {
+               sdb_events = lpcfg_dsdb_event_notification(lp_ctx);
+               pwd_events = lpcfg_dsdb_password_event_notification(lp_ctx);
+       }
+       if (sdb_events || pwd_events) {
+               context->send_samdb_events = sdb_events;
+               context->send_password_events = pwd_events;
+               context->msg_ctx = imessaging_client_init(ec, lp_ctx, ec);
+       }
+
+       ldb_module_set_private(module, context);
+       return ldb_next_init(module);
+}
+
+static const struct ldb_module_ops ldb_audit_log_module_ops = {
+       .name              = "audit_log",
+       .init_context      = log_init,
+       .add               = log_add,
+       .modify            = log_modify,
+       .del               = log_delete,
+       .start_transaction = log_start_transaction,
+       .prepare_commit    = log_prepare_commit,
+       .end_transaction   = log_end_transaction,
+       .del_transaction   = log_del_transaction,
+       .extended          = log_extended,
+};
+
+int ldb_audit_log_module_init(const char *version)
+{
+       LDB_MODULE_CHECK_VERSION(version);
+       return ldb_register_module(&ldb_audit_log_module_ops);
+}
diff --git a/source4/dsdb/samdb/ldb_modules/audit_util.c b/source4/dsdb/samdb/ldb_modules/audit_util.c
new file mode 100644 (file)
index 0000000..9e2d2d5
--- /dev/null
@@ -0,0 +1,596 @@
+/*
+   ldb database module utility library
+
+   Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+ * Common utility functions for SamDb audit logging.
+ *
+ */
+
+#include "includes.h"
+#include "ldb_module.h"
+#include "lib/audit_logging/audit_logging.h"
+
+#include "dsdb/samdb/samdb.h"
+#include "dsdb/samdb/ldb_modules/util.h"
+#include "libcli/security/dom_sid.h"
+#include "libcli/security/security_token.h"
+#include "auth/common_auth.h"
+#include "param/param.h"
+#include "dsdb/samdb/ldb_modules/util.h"
+
+#define MAX_LENGTH 1024
+
+#define min(a, b) (((a)>(b))?(b):(a))
+
+/*
+ * List of attributes considered secret or confidential the values of these
+ * attributes should not be displayed in log messages.
+ */
+static const char * const secret_attributes[] = {
+       DSDB_SECRET_ATTRIBUTES,
+       NULL};
+/*
+ * List of attributes that contain a password, used to detect password changes
+ */
+static const char * const password_attributes[] = {
+       DSDB_PASSWORD_ATTRIBUTES,
+       NULL};
+
+/*
+ * @brief Should the value of the specified value be redacted.
+ *
+ * The values of secret or password attributes should not be displayed.
+ *
+ * @param name The attributes name.
+ *
+ * @return True if the attribute should be redacted
+ */
+bool dsdb_audit_redact_attribute(const char * name)
+{
+
+       if (ldb_attr_in_list(secret_attributes, name)) {
+               return true;
+       }
+
+       if (ldb_attr_in_list(password_attributes, name)) {
+               return true;
+       }
+
+       return false;
+}
+
+/*
+ * @brief is the attribute a password attribute?
+ *
+ * Is the attribute a password attribute.
+ *
+ * @return True if the attribute is a "Password" attribute.
+ */
+bool dsdb_audit_is_password_attribute(const char * name)
+{
+
+       bool is_password = ldb_attr_in_list(password_attributes, name);
+       return is_password;
+}
+
+/*
+ * @brief Get the remote address from the ldb context.
+ *
+ * The remote address is stored in the ldb opaque value "remoteAddress"
+ * it is the responsibility of the higher level code to ensure that this
+ * value is set.
+ *
+ * @param ldb the ldb_context.
+ *
+ * @return the remote address if known, otherwise NULL.
+ */
+const struct tsocket_address *dsdb_audit_get_remote_address(
+       struct ldb_context *ldb)
+{
+       void *opaque_remote_address = NULL;
+       struct tsocket_address *remote_address;
+
+       opaque_remote_address = ldb_get_opaque(ldb,
+                                              "remoteAddress");
+       if (opaque_remote_address == NULL) {
+               return NULL;
+       }
+
+       remote_address = talloc_get_type(opaque_remote_address,
+                                        struct tsocket_address);
+       return remote_address;
+}
+
+/*
+ * @brief Get the actual user SID from ldb context.
+ *
+ * The actual user SID is stored in the ldb opaque value "networkSessionInfo"
+ * it is the responsibility of the higher level code to ensure that this
+ * value is set.
+ *
+ * @param ldb the ldb_context.
+ *
+ * @return the users actual sid.
+ */
+const struct dom_sid *dsdb_audit_get_actual_sid(struct ldb_context *ldb)
+{
+       void *opaque_session = NULL;
+       struct auth_session_info *session = NULL;
+       struct security_token *user_token = NULL;
+
+       opaque_session = ldb_get_opaque(ldb, "networkSessionInfo");
+       if (opaque_session == NULL) {
+               return NULL;
+       }
+
+       session = talloc_get_type(opaque_session, struct auth_session_info);
+       if (session == NULL) {
+               return NULL;
+       }
+
+       user_token = session->security_token;
+       if (user_token == NULL) {
+               return NULL;
+       }
+       return &user_token->sids[0];
+}
+/*
+ * @brief get the ldb error string.
+ *
+ * Get the ldb error string if set, otherwise get the generic error code
+ * for the status code.
+ *
+ * @param ldb the ldb_context.
+ * @param status the ldb_status code.
+ *
+ * @return a string describing the error.
+ */
+const char *dsdb_audit_get_ldb_error_string(
+       struct ldb_module *module,
+       int status)
+{
+       struct ldb_context *ldb = ldb_module_get_ctx(module);
+       const char *err_string = ldb_errstring(ldb);
+
+       if (err_string == NULL) {
+               return ldb_strerror(status);
+       }
+       return err_string;
+}
+
+/*
+ * @brief get the SID of the user performing the operation.
+ *
+ * Get the SID of the user performing the operation.
+ *
+ * @param module the ldb_module.
+ *
+ * @return the SID of the currently logged on user.
+ */
+const struct dom_sid *dsdb_audit_get_user_sid(const struct ldb_module *module)
+{
+       struct security_token *user_token = NULL;
+
+       /*
+        * acl_user_token does not alter module so it's safe
+        * to discard the const.
+        */
+       user_token = acl_user_token(discard_const(module));
+       if (user_token == NULL) {
+               return NULL;
+       }
+       return &user_token->sids[0];
+
+}
+
+/*
+ * @brief is operation being performed using the system session.
+ *
+ * Is the operation being performed using the system session.
+ *
+ * @param module the ldb_module.
+ *
+ * @return true if the operation is being performed using the system session.
+ */
+bool dsdb_audit_is_system_session(const struct ldb_module *module)
+{
+       struct security_token *user_token = NULL;
+
+       /*
+        * acl_user_token does not alter module and security_token_is_system
+        * does not alter the security token so it's safe to discard the const.
+        */
+       user_token = acl_user_token(discard_const(module));
+       if (user_token == NULL) {
+               return false;
+       }
+       return security_token_is_system(user_token);;
+
+}
+
+/*
+ * @brief get the session identifier GUID
+ *
+ * Get the GUID that uniquely identifies the current authenticated session.
+ *
+ * @param module the ldb_module.
+ *
+ * @return the unique session GUID
+ */
+const struct GUID *dsdb_audit_get_unique_session_token(
+       const struct ldb_module *module)
+{
+       struct ldb_context *ldb = ldb_module_get_ctx(discard_const(module));
+       struct auth_session_info *session_info
+               = (struct auth_session_info *)ldb_get_opaque(
+                       ldb,
+                       "sessionInfo");
+       if(!session_info) {
+               return NULL;
+       }
+       return &session_info->unique_session_token;
+}
+
+/*
+ * @brief get the actual user session identifier
+ *
+ * Get the GUID that uniquely identifies the current authenticated session.
+ * This is the session of the connected user, as it may differ from the
+ * session the operation is being performed as, i.e. for operations performed
+ * under the system session.
+ *
+ * @param context the ldb_context.
+ *
+ * @return the unique session GUID
+ */
+const struct GUID *dsdb_audit_get_actual_unique_session_token(
+       struct ldb_context *ldb)
+{
+       struct auth_session_info *session_info
+               = (struct auth_session_info *)ldb_get_opaque(
+                       ldb,
+                       "networkSessionInfo");
+       if(!session_info) {
+               return NULL;
+       }
+       return &session_info->unique_session_token;
+}
+
+/*
+ * @brief Get a printable string value for the remote host address.
+ *
+ * Get a printable string representation of the remote host, for display in the
+ * the audit logs.
+ *
+ * @param ldb the ldb context.
+ * @param mem_ctx the talloc memory context that will own the returned string.
+ *
+ * @return A string representation of the remote host address or "Unknown"
+ *
+ */
+char *dsdb_audit_get_remote_host(struct ldb_context *ldb, TALLOC_CTX *mem_ctx)
+{
+       const struct tsocket_address *remote_address;
+       char* remote_host = NULL;
+
+       remote_address = dsdb_audit_get_remote_address(ldb);
+       if (remote_address == NULL) {
+               remote_host = talloc_asprintf(mem_ctx, "Unknown");
+               return remote_host;
+       }
+
+       remote_host = tsocket_address_string(remote_address, mem_ctx);
+       return remote_host;
+}
+
+/*
+ * @brief get a printable representation of the primary DN.
+ *
+ * Get a printable representation of the primary DN. The primary DN is the
+ * DN of the object being added, deleted, modified or renamed.
+ *
+ * @param the ldb_request.
+ *
+ * @return a printable and linearized DN
+ */
+const char* dsdb_audit_get_primary_dn(const struct ldb_request *request)
+{
+       struct ldb_dn *dn = NULL;
+       switch (request->operation) {
+       case LDB_ADD:
+               if (request->op.add.message != NULL) {
+                       dn = request->op.add.message->dn;
+               }
+               break;
+       case LDB_MODIFY:
+               if (request->op.mod.message != NULL) {
+                       dn = request->op.mod.message->dn;
+               }
+               break;
+       case LDB_DELETE:
+               dn = request->op.del.dn;
+               break;
+       case LDB_RENAME:
+               dn = request->op.rename.olddn;
+               break;
+       default:
+               dn = NULL;
+               break;
+       }
+       if (dn == NULL) {
+               return NULL;
+       }
+       return ldb_dn_get_linearized(dn);
+}
+
+/*
+ * @brief Get the ldb_message from a request.
+ *
+ * Get the ldb_message for the request, returns NULL is there is no
+ * associated ldb_message
+ *
+ * @param The request
+ *
+ * @return the message associated with this request, or NULL
+ */
+const struct ldb_message *dsdb_audit_get_message(
+       const struct ldb_request *request)
+{
+       switch (request->operation) {
+       case LDB_ADD:
+               return request->op.add.message;
+       case LDB_MODIFY:
+               return request->op.mod.message;
+       default:
+               return NULL;
+       }
+}
+
+/*
+ * @brief get the secondary dn, i.e. the target dn for a rename.
+ *
+ * Get the secondary dn, i.e. the target for a rename. This is only applicable
+ * got a rename operation, for the non rename operations this function returns
+ * NULL.
+ *
+ * @param request the ldb_request.
+ *
+ * @return the secondary dn in a printable and linearized form.
+ */
+const char *dsdb_audit_get_secondary_dn(const struct ldb_request *request)
+{
+       switch (request->operation) {
+       case LDB_RENAME:
+               return ldb_dn_get_linearized(request->op.rename.newdn);
+       default:
+               return NULL;
+       }
+}
+
+/*
+ * @brief Map the request operation to a description.
+ *
+ * Get a description of the operation for logging
+ *
+ * @param request the ldb_request
+ *
+ * @return a string describing the operation, or "Unknown" if the operation
+ *         is not known.
+ */
+const char *dsdb_audit_get_operation_name(const struct ldb_request *request)
+{
+       switch (request->operation) {
+       case LDB_SEARCH:
+               return "Search";
+       case LDB_ADD:
+               return "Add";
+       case LDB_MODIFY:
+               return "Modify";
+       case LDB_DELETE:
+               return "Delete";
+       case LDB_RENAME:
+               return "Rename";
+       case LDB_EXTENDED:
+               return "Extended";
+       case LDB_REQ_REGISTER_CONTROL:
+               return "Register Control";
+       case LDB_REQ_REGISTER_PARTITION:
+               return "Register Partition";
+       default:
+               return "Unknown";
+       }
+}
+
+/*
+ * @brief get a description of a modify action for logging.
+ *
+ * Get a brief description of the modification action suitable for logging.
+ *
+ * @param flags the ldb_attributes flags.
+ *
+ * @return a brief description, or "unknown".
+ */
+const char *dsdb_audit_get_modification_action(unsigned int flags)
+{
+       switch (LDB_FLAG_MOD_TYPE(flags)) {
+       case LDB_FLAG_MOD_ADD:
+               return "add";
+       case LDB_FLAG_MOD_DELETE:
+               return "delete";
+       case LDB_FLAG_MOD_REPLACE:
+               return "replace";
+       default:
+               return "unknown";
+       }
+}
+
+/*
+ * @brief Add an ldb_value to a json object array
+ *
+ * Convert the current ldb_value to a JSON object and append it to array.
+ * {
+ *     "value":"xxxxxxxx",
+ *     "base64":true
+ *     "truncated":true
+ * }
+ *
+ * value     is the JSON string representation of the ldb_val,
+ *           will be null if the value is zero length. The value will be
+ *           truncated if it is more than MAX_LENGTH bytes long. It will also
+ *           be base64 encoded if it contains any non printable characters.
+ *
+ * base64    Indicates that the value is base64 encoded, will be absent if the
+ *           value is not encoded.
+ *
+ * truncated Indicates that the length of the value exceeded MAX_LENGTH and was
+ *           truncated.  Note that vales are truncated and then base64 encoded.
+ *           so an encoded value can be longer than MAX_LENGTH.
+ *
+ * @param array the JSON array to append the value to.
+ * @param lv the ldb_val to convert and append to the array.
+ *
+ */
+static void dsdb_audit_add_ldb_value(
+       struct json_object *array,
+       const struct ldb_val lv)
+{
+
+       json_assert_is_array(array);
+       if (json_is_invalid(array)) {
+               return;
+       }
+
+       if (lv.length == 0 || lv.data == NULL) {
+               json_add_object(array, NULL, NULL);
+               return;
+       }
+
+       bool base64 = ldb_should_b64_encode(NULL, &lv);
+       int len = min(lv.length, MAX_LENGTH);
+       struct json_object value = json_new_object();
+       if (lv.length > MAX_LENGTH) {
+               json_add_bool(&value, "truncated", true);
+       }
+       if (base64) {
+               TALLOC_CTX *ctx = talloc_new(NULL);
+               char *encoded = ldb_base64_encode(
+                       ctx,
+                       (char*) lv.data,
+                       len);
+
+               json_add_bool(&value, "base64", true);
+               json_add_string(&value, "value", encoded);
+               TALLOC_FREE(ctx);
+       } else {
+               json_add_stringn(&value, "value", (char *)lv.data, len);
+       }
+       /*
+        * As array is a JSON array the element name is NULL
+        */
+       json_add_object(array, NULL, &value);
+}
+
+/*
+ * @brief Build a JSON object containing the attributes in an ldb_message.
+ *
+ * Build a JSON object containing all the attributes in an ldb_message.
+ * The attributes are keyed by attribute name, the values of "secret attributes"
+ * are supressed.
+ *
+ * {
+ *     "password":{
+ *             "redacted":true,
+ *             "action":"delete"
+ *     },
+ *     "name":{
+ *             "values": [
+ *                     {
+ *                             "value":"xxxxxxxx",
+ *                             "base64":true
+ *                             "truncated":true
+ *                     },
+ *             ],
+ *             "action":"add",
+ *     }
+ * }
+ *
+ * values is an array of json objects generated by add_ldb_value.
+ * redacted indicates that the attribute is secret.
+ * action is only set for modification operations.
+ *
+ * @param operation the ldb operation being performed
+ * @param message the ldb_message to process.
+ *
+ * @return A populated json object.
+ *
+ */
+struct json_object dsdb_audit_attributes_json(
+       enum ldb_request_type operation,
+       const struct ldb_message* message)
+{
+
+       struct json_object attributes = json_new_object();
+       int i, j;
+       for (i=0;i<message->num_elements;i++) {
+               struct json_object actions;
+               struct json_object attribute;
+               struct json_object action = json_new_object();
+               const char *name = message->elements[i].name;
+
+               /*
+                * If this is a modify operation tag the attribute with
+                * the modification action.
+                */
+               if (operation == LDB_MODIFY) {
+                       const char *act = NULL;
+                       const int flags =  message->elements[i].flags;
+                       act = dsdb_audit_get_modification_action(flags);
+                       json_add_string(&action, "action" , act);
+               }
+               if (operation == LDB_ADD) {
+                       json_add_string(&action, "action" , "add");
+               }
+
+               /*
+                * If the attribute is a secret attribute, tag it as redacted
+                * and don't include the values
+                */
+               if (dsdb_audit_redact_attribute(name)) {
+                       json_add_bool(&action, "redacted", true);
+               } else {
+                       struct json_object values;
+                       /*
+                        * Add the values for the action
+                        */
+                       values = json_new_array();
+                       for (j=0;j<message->elements[i].num_values;j++) {
+                               dsdb_audit_add_ldb_value(
+                                       &values,
+                                       message->elements[i].values[j]);
+                       }
+                       json_add_object(&action, "values", &values);
+               }
+               attribute = json_get_object(&attributes, name);
+               actions = json_get_array(&attribute, "actions");
+               json_add_object(&actions, NULL, &action);
+               json_add_object(&attribute, "actions", &actions);
+               json_add_object(&attributes, name, &attribute);
+       }
+       return attributes;
+}
index 54ec6a26068f0dc8cebbb75714503e3af34806af..baa30f9748fdb29a23798af09921d086b0dc2fc9 100644 (file)
@@ -292,7 +292,8 @@ static int samba_dsdb_init(struct ldb_module *module)
                                             "extended_dn_store",
                                             NULL };
        /* extended_dn_in or extended_dn_in_openldap goes here */
-       static const char *modules_list1a[] = {"objectclass",
+       static const char *modules_list1a[] = {"audit_log",
+                                            "objectclass",
                                             "tombstone_reanimate",
                                             "descriptor",
                                             "acl",
diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_audit_log.c b/source4/dsdb/samdb/ldb_modules/tests/test_audit_log.c
new file mode 100644 (file)
index 0000000..1e35504
--- /dev/null
@@ -0,0 +1,2243 @@
+/*
+   Unit tests for the dsdb audit logging code code in audit_log.c
+
+   Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <unistd.h>
+#include <cmocka.h>
+
+int ldb_audit_log_module_init(const char *version);
+#include "../audit_log.c"
+
+#include "lib/ldb/include/ldb_private.h"
+#include <regex.h>
+
+/*
+ * Test helper to check ISO 8601 timestamps for validity
+ */
+static void check_timestamp(time_t before, const char* timestamp)
+{
+       int rc;
+       int usec, tz;
+       char c[2];
+       struct tm tm;
+       time_t after;
+       time_t actual;
+
+
+       after = time(NULL);
+
+       /*
+        * Convert the ISO 8601 timestamp into a time_t
+        * Note for convenience we ignore the value of the microsecond
+        * part of the time stamp.
+        */
+       rc = sscanf(
+               timestamp,
+               "%4d-%2d-%2dT%2d:%2d:%2d.%6d%1c%4d",
+               &tm.tm_year,
+               &tm.tm_mon,
+               &tm.tm_mday,
+               &tm.tm_hour,
+               &tm.tm_min,
+               &tm.tm_sec,
+               &usec,
+               c,
+               &tz);
+       assert_int_equal(9, rc);
+       tm.tm_year = tm.tm_year - 1900;
+       tm.tm_mon = tm.tm_mon - 1;
+       tm.tm_isdst = -1;
+       actual = mktime(&tm);
+
+       /*
+        * The timestamp should be before <= actual <= after
+        */
+       assert_true(difftime(actual, before) >= 0);
+       assert_true(difftime(after, actual) >= 0);
+}
+
+static void test_has_password_changed(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_message *msg = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       /*
+        * Empty message
+        */
+       msg = ldb_msg_new(ldb);
+       assert_false(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        * No password attributes
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "attr01", "value01");
+       assert_false(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        * No password attributes >1 entries
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "attr01", "value01");
+       ldb_msg_add_string(msg, "attr02", "value03");
+       ldb_msg_add_string(msg, "attr03", "value03");
+       assert_false(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        *  userPassword set
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "userPassword", "value01");
+       assert_true(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        *  clearTextPassword set
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "clearTextPassword", "value01");
+       assert_true(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        *  unicodePwd set
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "unicodePwd", "value01");
+       assert_true(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        *  dBCSPwd set
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "dBCSPwd", "value01");
+       assert_true(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        *  All attributes set
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "userPassword", "value01");
+       ldb_msg_add_string(msg, "clearTextPassword", "value02");
+       ldb_msg_add_string(msg, "unicodePwd", "value03");
+       ldb_msg_add_string(msg, "dBCSPwd", "value04");
+       assert_true(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        *  first attribute is a password attribute
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "userPassword", "value01");
+       ldb_msg_add_string(msg, "attr02", "value02");
+       ldb_msg_add_string(msg, "attr03", "value03");
+       ldb_msg_add_string(msg, "attr04", "value04");
+       assert_true(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        *  last attribute is a password attribute
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "attr01", "value01");
+       ldb_msg_add_string(msg, "attr02", "value02");
+       ldb_msg_add_string(msg, "attr03", "value03");
+       ldb_msg_add_string(msg, "clearTextPassword", "value04");
+       assert_true(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       /*
+        *  middle attribute is a password attribute
+        */
+       msg = ldb_msg_new(ldb);
+       ldb_msg_add_string(msg, "attr01", "value01");
+       ldb_msg_add_string(msg, "attr02", "value02");
+       ldb_msg_add_string(msg, "unicodePwd", "pwd");
+       ldb_msg_add_string(msg, "attr03", "value03");
+       ldb_msg_add_string(msg, "attr04", "value04");
+       assert_true(has_password_changed(msg));
+       TALLOC_FREE(msg);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_get_password_action(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct dsdb_control_password_acl_validation *pav = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       /*
+        * Add request, will always be a reset
+        */
+       ldb_build_add_req(&req, ldb, ctx, NULL, NULL, NULL, NULL, NULL);
+       reply = talloc_zero(ctx, struct ldb_reply);
+       assert_string_equal("Reset", get_password_action(req, reply));
+       TALLOC_FREE(req);
+       TALLOC_FREE(reply);
+
+       /*
+        * No password control acl, expect "Reset"
+        */
+       ldb_build_mod_req(&req, ldb, ctx, NULL, NULL, NULL, NULL, NULL);
+       reply = talloc_zero(ctx, struct ldb_reply);
+       assert_string_equal("Reset", get_password_action(req, reply));
+       TALLOC_FREE(req);
+       TALLOC_FREE(reply);
+
+       /*
+        * dsdb_control_password_acl_validation reset = false, expect "Change"
+        */
+       ldb_build_mod_req(&req, ldb, ctx, NULL, NULL, NULL, NULL, NULL);
+       reply = talloc_zero(ctx, struct ldb_reply);
+       pav = talloc_zero(req, struct dsdb_control_password_acl_validation);
+
+       ldb_reply_add_control(
+               reply,
+               DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID,
+               false,
+               pav);
+       assert_string_equal("Change", get_password_action(req, reply));
+       TALLOC_FREE(req);
+       TALLOC_FREE(reply);
+
+       /*
+        * dsdb_control_password_acl_validation reset = true, expect "Reset"
+        */
+       ldb_build_mod_req(&req, ldb, ctx, NULL, NULL, NULL, NULL, NULL);
+       reply = talloc_zero(ctx, struct ldb_reply);
+       pav = talloc_zero(req, struct dsdb_control_password_acl_validation);
+       pav->pwd_reset = true;
+
+       ldb_reply_add_control(
+               reply,
+               DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID,
+               false,
+               pav);
+       assert_string_equal("Reset", get_password_action(req, reply));
+       TALLOC_FREE(req);
+       TALLOC_FREE(reply);
+
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * Test helper to validate a version object.
+ */
+static void check_version(struct json_t *version, int major, int minor)
+{
+       struct json_t *v = NULL;
+
+       assert_true(json_is_object(version));
+       assert_int_equal(2, json_object_size(version));
+
+       v = json_object_get(version, "major");
+       assert_non_null(v);
+       assert_int_equal(major, json_integer_value(v));
+
+       v = json_object_get(version, "minor");
+       assert_non_null(v);
+       assert_int_equal(minor, json_integer_value(v));
+}
+
+/*
+ * minimal unit test of operation_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_operation_json_empty(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       json_t *o = NULL;
+       time_t before;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       ac = talloc_zero(ctx, struct audit_context);
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       req = talloc_zero(ctx, struct ldb_request);
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       before = time(NULL);
+       json = operation_json(module, req, reply);
+       assert_int_equal(3, json_object_size(json.root));
+
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("dsdbChange", json_string_value(v));
+
+       v = json_object_get(json.root, "timestamp");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       check_timestamp(before, json_string_value(v));
+
+       audit = json_object_get(json.root, "dsdbChange");
+       assert_non_null(audit);
+       assert_true(json_is_object(audit));
+       assert_int_equal(10, json_object_size(audit));
+
+       o = json_object_get(audit, "version");
+       assert_non_null(o);
+       check_version(o, OPERATION_MAJOR, OPERATION_MINOR);
+
+       v = json_object_get(audit, "statusCode");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(LDB_SUCCESS, json_integer_value(v));
+
+       v = json_object_get(audit, "status");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("Success", json_string_value(v));
+
+       v = json_object_get(audit, "operation");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       /*
+        * Search operation constant is zero
+        */
+       assert_string_equal("Search", json_string_value(v));
+
+       v = json_object_get(audit, "remoteAddress");
+       assert_non_null(v);
+       assert_true(json_is_null(v));
+
+       v = json_object_get(audit, "userSid");
+       assert_non_null(v);
+       assert_true(json_is_null(v));
+
+       v = json_object_get(audit, "performedAsSystem");
+       assert_non_null(v);
+       assert_true(json_is_boolean(v));
+       assert_true(json_is_false(v));
+
+
+       v = json_object_get(audit, "dn");
+       assert_non_null(v);
+       assert_true(json_is_null(v));
+
+       v = json_object_get(audit, "transactionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(
+               "00000000-0000-0000-0000-000000000000",
+               json_string_value(v));
+
+       v = json_object_get(audit, "sessionId");
+       assert_non_null(v);
+       assert_true(json_is_null(v));
+
+       json_free(&json);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * unit test of operation_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_operation_json(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       struct tsocket_address *ts = NULL;
+
+       struct auth_session_info *sess = NULL;
+       struct security_token *token = NULL;
+       struct dom_sid sid;
+       const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       struct GUID session_id;
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct ldb_dn *dn = NULL;
+       const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+
+       struct ldb_message *msg = NULL;
+
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       json_t *o = NULL;
+       json_t *a = NULL;
+       json_t *b = NULL;
+       json_t *c = NULL;
+       json_t *d = NULL;
+       json_t *e = NULL;
+       json_t *f = NULL;
+       json_t *g = NULL;
+       time_t before;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       ac = talloc_zero(ctx, struct audit_context);
+       GUID_from_string(TRANSACTION, &transaction_id);
+       ac->transaction_guid = transaction_id;
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts);
+       ldb_set_opaque(ldb, "remoteAddress", ts);
+
+       sess = talloc_zero(ctx, struct auth_session_info);
+       token = talloc_zero(ctx, struct security_token);
+       string_to_sid(&sid, SID);
+       token->num_sids = 1;
+       token->sids = &sid;
+       sess->security_token = token;
+       GUID_from_string(SESSION, &session_id);
+       sess->unique_session_token = session_id;
+       ldb_set_opaque(ldb, "sessionInfo", sess);
+
+       msg = talloc_zero(ctx, struct ldb_message);
+       dn = ldb_dn_new(ctx, ldb, DN);
+       msg->dn = dn;
+       ldb_msg_add_string(msg, "attribute", "the-value");
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       req->op.add.message = msg;
+
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_ERR_OPERATIONS_ERROR;
+
+       before = time(NULL);
+       json = operation_json(module, req, reply);
+       assert_int_equal(3, json_object_size(json.root));
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("dsdbChange", json_string_value(v));
+
+       v = json_object_get(json.root, "timestamp");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       check_timestamp(before, json_string_value(v));
+
+       audit = json_object_get(json.root, "dsdbChange");
+       assert_non_null(audit);
+       assert_true(json_is_object(audit));
+       assert_int_equal(11, json_object_size(audit));
+
+       o = json_object_get(audit, "version");
+       assert_non_null(o);
+       check_version(o, OPERATION_MAJOR, OPERATION_MINOR);
+
+       v = json_object_get(audit, "statusCode");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(LDB_ERR_OPERATIONS_ERROR, json_integer_value(v));
+
+       v = json_object_get(audit, "status");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("Operations error", json_string_value(v));
+
+       v = json_object_get(audit, "operation");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("Add", json_string_value(v));
+
+       v = json_object_get(audit, "remoteAddress");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("ipv4:127.0.0.1:0", json_string_value(v));
+
+       v = json_object_get(audit, "userSid");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(SID, json_string_value(v));
+
+       v = json_object_get(audit, "performedAsSystem");
+       assert_non_null(v);
+       assert_true(json_is_boolean(v));
+       assert_true(json_is_false(v));
+
+       v = json_object_get(audit, "dn");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(DN, json_string_value(v));
+
+       v = json_object_get(audit, "transactionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(TRANSACTION, json_string_value(v));
+
+       v = json_object_get(audit, "sessionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(SESSION, json_string_value(v));
+
+       o = json_object_get(audit, "attributes");
+       assert_non_null(v);
+       assert_true(json_is_object(o));
+       assert_int_equal(1, json_object_size(o));
+
+       a = json_object_get(o, "attribute");
+       assert_non_null(a);
+       assert_true(json_is_object(a));
+
+       b = json_object_get(a, "actions");
+       assert_non_null(b);
+       assert_true(json_is_array(b));
+       assert_int_equal(1, json_array_size(b));
+
+       c = json_array_get(b, 0);
+       assert_non_null(c);
+       assert_true(json_is_object(c));
+
+       d = json_object_get(c, "action");
+       assert_non_null(d);
+       assert_true(json_is_string(d));
+       assert_string_equal("add", json_string_value(d));
+
+       e = json_object_get(c, "values");
+       assert_non_null(b);
+       assert_true(json_is_array(e));
+       assert_int_equal(1, json_array_size(e));
+
+       f = json_array_get(e, 0);
+       assert_non_null(f);
+       assert_true(json_is_object(f));
+       assert_int_equal(1, json_object_size(f));
+
+       g = json_object_get(f, "value");
+       assert_non_null(g);
+       assert_true(json_is_string(g));
+       assert_string_equal("the-value", json_string_value(g));
+
+       json_free(&json);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * unit test of operation_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ * In this case for an operation performed as the system user.
+ */
+static void test_as_system_operation_json(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       struct tsocket_address *ts = NULL;
+
+       struct auth_session_info *sess = NULL;
+       struct auth_session_info *sys_sess = NULL;
+       struct security_token *token = NULL;
+       struct security_token *sys_token = NULL;
+       struct dom_sid sid;
+       const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const SYS_SESSION = "7130cb06-2062-6a1b-409e-3514c26b1998";
+       struct GUID session_id;
+       struct GUID sys_session_id;
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct ldb_dn *dn = NULL;
+       const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+
+       struct ldb_message *msg = NULL;
+
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       json_t *o = NULL;
+       json_t *a = NULL;
+       json_t *b = NULL;
+       json_t *c = NULL;
+       json_t *d = NULL;
+       json_t *e = NULL;
+       json_t *f = NULL;
+       json_t *g = NULL;
+       time_t before;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       ac = talloc_zero(ctx, struct audit_context);
+       GUID_from_string(TRANSACTION, &transaction_id);
+       ac->transaction_guid = transaction_id;
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts);
+       ldb_set_opaque(ldb, "remoteAddress", ts);
+
+       sess = talloc_zero(ctx, struct auth_session_info);
+       token = talloc_zero(ctx, struct security_token);
+       string_to_sid(&sid, SID);
+       token->num_sids = 1;
+       token->sids = &sid;
+       sess->security_token = token;
+       GUID_from_string(SESSION, &session_id);
+       sess->unique_session_token = session_id;
+       ldb_set_opaque(ldb, "networkSessionInfo", sess);
+
+       sys_sess = talloc_zero(ctx, struct auth_session_info);
+       sys_token = talloc_zero(ctx, struct security_token);
+       sys_token->num_sids = 1;
+       sys_token->sids = discard_const(&global_sid_System);
+       sys_sess->security_token = sys_token;
+       GUID_from_string(SYS_SESSION, &sys_session_id);
+       sess->unique_session_token = sys_session_id;
+       ldb_set_opaque(ldb, "sessionInfo", sys_sess);
+
+       msg = talloc_zero(ctx, struct ldb_message);
+       dn = ldb_dn_new(ctx, ldb, DN);
+       msg->dn = dn;
+       ldb_msg_add_string(msg, "attribute", "the-value");
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       req->op.add.message = msg;
+
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_ERR_OPERATIONS_ERROR;
+
+       before = time(NULL);
+       json = operation_json(module, req, reply);
+       assert_int_equal(3, json_object_size(json.root));
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("dsdbChange", json_string_value(v));
+
+       v = json_object_get(json.root, "timestamp");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       check_timestamp(before, json_string_value(v));
+
+       audit = json_object_get(json.root, "dsdbChange");
+       assert_non_null(audit);
+       assert_true(json_is_object(audit));
+       assert_int_equal(11, json_object_size(audit));
+
+       o = json_object_get(audit, "version");
+       assert_non_null(o);
+       check_version(o, OPERATION_MAJOR, OPERATION_MINOR);
+
+       v = json_object_get(audit, "statusCode");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(LDB_ERR_OPERATIONS_ERROR, json_integer_value(v));
+
+       v = json_object_get(audit, "status");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("Operations error", json_string_value(v));
+
+       v = json_object_get(audit, "operation");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("Add", json_string_value(v));
+
+       v = json_object_get(audit, "remoteAddress");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("ipv4:127.0.0.1:0", json_string_value(v));
+
+       v = json_object_get(audit, "userSid");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(SID, json_string_value(v));
+
+       v = json_object_get(audit, "performedAsSystem");
+       assert_non_null(v);
+       assert_true(json_is_boolean(v));
+       assert_true(json_is_true(v));
+
+       v = json_object_get(audit, "dn");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(DN, json_string_value(v));
+
+       v = json_object_get(audit, "transactionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(TRANSACTION, json_string_value(v));
+
+       v = json_object_get(audit, "sessionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(SYS_SESSION, json_string_value(v));
+
+       o = json_object_get(audit, "attributes");
+       assert_non_null(v);
+       assert_true(json_is_object(o));
+       assert_int_equal(1, json_object_size(o));
+
+       a = json_object_get(o, "attribute");
+       assert_non_null(a);
+       assert_true(json_is_object(a));
+
+       b = json_object_get(a, "actions");
+       assert_non_null(b);
+       assert_true(json_is_array(b));
+       assert_int_equal(1, json_array_size(b));
+
+       c = json_array_get(b, 0);
+       assert_non_null(c);
+       assert_true(json_is_object(c));
+
+       d = json_object_get(c, "action");
+       assert_non_null(d);
+       assert_true(json_is_string(d));
+       assert_string_equal("add", json_string_value(d));
+
+       e = json_object_get(c, "values");
+       assert_non_null(b);
+       assert_true(json_is_array(e));
+       assert_int_equal(1, json_array_size(e));
+
+       f = json_array_get(e, 0);
+       assert_non_null(f);
+       assert_true(json_is_object(f));
+       assert_int_equal(1, json_object_size(f));
+
+       g = json_object_get(f, "value");
+       assert_non_null(g);
+       assert_true(json_is_string(g));
+       assert_string_equal("the-value", json_string_value(g));
+
+       json_free(&json);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * minimal unit test of password_change_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_password_change_json_empty(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       json_t *o = NULL;
+       time_t before;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       ac = talloc_zero(ctx, struct audit_context);
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       req = talloc_zero(ctx, struct ldb_request);
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       before = time(NULL);
+       json = password_change_json(module, req, reply);
+       assert_int_equal(3, json_object_size(json.root));
+
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("passwordChange", json_string_value(v));
+
+       v = json_object_get(json.root, "timestamp");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       check_timestamp(before, json_string_value(v));
+
+       audit = json_object_get(json.root, "passwordChange");
+       assert_non_null(audit);
+       assert_true(json_is_object(audit));
+       assert_int_equal(9, json_object_size(audit));
+
+       o = json_object_get(audit, "version");
+       assert_non_null(o);
+
+       v = json_object_get(audit, "statusCode");
+       assert_non_null(v);
+
+       v = json_object_get(audit, "status");
+       assert_non_null(v);
+
+       v = json_object_get(audit, "remoteAddress");
+       assert_non_null(v);
+
+       v = json_object_get(audit, "userSid");
+       assert_non_null(v);
+
+       v = json_object_get(audit, "dn");
+       assert_non_null(v);
+
+       v = json_object_get(audit, "transactionId");
+       assert_non_null(v);
+
+       v = json_object_get(audit, "sessionId");
+       assert_non_null(v);
+
+       v = json_object_get(audit, "action");
+       assert_non_null(v);
+
+       json_free(&json);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * minimal unit test of password_change_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_password_change_json(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       struct tsocket_address *ts = NULL;
+
+       struct auth_session_info *sess = NULL;
+       struct security_token *token = NULL;
+       struct dom_sid sid;
+       const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       struct GUID session_id;
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct ldb_dn *dn = NULL;
+       const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+
+       struct ldb_message *msg = NULL;
+
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       json_t *o = NULL;
+       time_t before;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       ac = talloc_zero(ctx, struct audit_context);
+       GUID_from_string(TRANSACTION, &transaction_id);
+       ac->transaction_guid = transaction_id;
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts);
+       ldb_set_opaque(ldb, "remoteAddress", ts);
+
+       sess = talloc_zero(ctx, struct auth_session_info);
+       token = talloc_zero(ctx, struct security_token);
+       string_to_sid(&sid, SID);
+       token->num_sids = 1;
+       token->sids = &sid;
+       sess->security_token = token;
+       GUID_from_string(SESSION, &session_id);
+       sess->unique_session_token = session_id;
+       ldb_set_opaque(ldb, "sessionInfo", sess);
+
+       msg = talloc_zero(ctx, struct ldb_message);
+       dn = ldb_dn_new(ctx, ldb, DN);
+       msg->dn = dn;
+       ldb_msg_add_string(msg, "planTextPassword", "super-secret");
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       req->op.add.message = msg;
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       before = time(NULL);
+       json = password_change_json(module, req, reply);
+       assert_int_equal(3, json_object_size(json.root));
+
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("passwordChange", json_string_value(v));
+
+       v = json_object_get(json.root, "timestamp");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       check_timestamp(before, json_string_value(v));
+
+       audit = json_object_get(json.root, "passwordChange");
+       assert_non_null(audit);
+       assert_true(json_is_object(audit));
+       assert_int_equal(9, json_object_size(audit));
+
+       o = json_object_get(audit, "version");
+       assert_non_null(o);
+       check_version(o, PASSWORD_MAJOR,PASSWORD_MINOR);
+
+       v = json_object_get(audit, "statusCode");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(LDB_SUCCESS, json_integer_value(v));
+
+       v = json_object_get(audit, "status");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("Success", json_string_value(v));
+
+       v = json_object_get(audit, "remoteAddress");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("ipv4:127.0.0.1:0", json_string_value(v));
+
+       v = json_object_get(audit, "userSid");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(SID, json_string_value(v));
+
+       v = json_object_get(audit, "dn");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(DN, json_string_value(v));
+
+       v = json_object_get(audit, "transactionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(TRANSACTION, json_string_value(v));
+
+       v = json_object_get(audit, "sessionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(SESSION, json_string_value(v));
+
+       v = json_object_get(audit, "action");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("Reset", json_string_value(v));
+
+       json_free(&json);
+       TALLOC_FREE(ctx);
+
+}
+
+
+/*
+ * minimal unit test of transaction_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_transaction_json(void **state)
+{
+
+       struct GUID guid;
+       const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       json_t *o = NULL;
+       time_t before;
+
+       GUID_from_string(GUID, &guid);
+
+       before = time(NULL);
+       json = transaction_json("delete", &guid);
+
+       assert_int_equal(3, json_object_size(json.root));
+
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("dsdbTransaction", json_string_value(v));
+
+       v = json_object_get(json.root, "timestamp");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       check_timestamp(before, json_string_value(v));
+
+       audit = json_object_get(json.root, "dsdbTransaction");
+       assert_non_null(audit);
+       assert_true(json_is_object(audit));
+       assert_int_equal(3, json_object_size(audit));
+
+       o = json_object_get(audit, "version");
+       assert_non_null(o);
+       check_version(o, TRANSACTION_MAJOR, TRANSACTION_MINOR);
+
+       v = json_object_get(audit, "transactionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(GUID, json_string_value(v));
+
+       v = json_object_get(audit, "action");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("delete", json_string_value(v));
+
+       json_free(&json);
+
+}
+
+/*
+ * minimal unit test of commit_failure_json, that ensures that all the
+ * expected attributes and objects are in the json object.
+ */
+static void test_commit_failure_json(void **state)
+{
+
+       struct GUID guid;
+       const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       json_t *o = NULL;
+       time_t before;
+
+       GUID_from_string(GUID, &guid);
+
+       before = time(NULL);
+       json = commit_failure_json(
+               "prepare",
+               LDB_ERR_OPERATIONS_ERROR,
+               "because",
+               &guid);
+
+       assert_int_equal(3, json_object_size(json.root));
+
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("dsdbTransaction", json_string_value(v));
+
+       v = json_object_get(json.root, "timestamp");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       check_timestamp(before, json_string_value(v));
+
+       audit = json_object_get(json.root, "dsdbTransaction");
+       assert_non_null(audit);
+       assert_true(json_is_object(audit));
+       assert_int_equal(6, json_object_size(audit));
+
+       o = json_object_get(audit, "version");
+       assert_non_null(o);
+       check_version(o, TRANSACTION_MAJOR, TRANSACTION_MINOR);
+
+       v = json_object_get(audit, "transactionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(GUID, json_string_value(v));
+
+       v = json_object_get(audit, "action");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("prepare", json_string_value(v));
+
+       v = json_object_get(audit, "statusCode");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(LDB_ERR_OPERATIONS_ERROR, json_integer_value(v));
+
+       v = json_object_get(audit, "status");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("Operations error", json_string_value(v));
+       v = json_object_get(audit, "status");
+       assert_non_null(v);
+
+       v = json_object_get(audit, "reason");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("because", json_string_value(v));
+
+       json_free(&json);
+
+}
+
+/*
+ * minimal unit test of replicated_update_json, that ensures that all the
+ * expected attributes and objects are in the json object.
+ */
+static void test_replicated_update_json_empty(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+       struct dsdb_extended_replicated_objects *ro = NULL;
+       struct repsFromTo1 *source_dsa = NULL;
+
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       json_t *o = NULL;
+       time_t before;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       ac = talloc_zero(ctx, struct audit_context);
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       source_dsa = talloc_zero(ctx, struct repsFromTo1);
+       ro = talloc_zero(ctx, struct dsdb_extended_replicated_objects);
+       ro->source_dsa = source_dsa;
+       req = talloc_zero(ctx, struct ldb_request);
+       req->op.extended.data = ro;
+       req->operation = LDB_EXTENDED;
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       before = time(NULL);
+       json = replicated_update_json(module, req, reply);
+       assert_int_equal(3, json_object_size(json.root));
+
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("replicatedUpdate", json_string_value(v));
+
+       v = json_object_get(json.root, "timestamp");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       check_timestamp(before, json_string_value(v));
+
+       audit = json_object_get(json.root, "replicatedUpdate");
+       assert_non_null(audit);
+       assert_true(json_is_object(audit));
+       assert_int_equal(11, json_object_size(audit));
+
+       o = json_object_get(audit, "version");
+       assert_non_null(o);
+       check_version(o, REPLICATION_MAJOR, REPLICATION_MINOR);
+
+       v = json_object_get(audit, "statusCode");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(LDB_SUCCESS, json_integer_value(v));
+
+       v = json_object_get(audit, "status");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("Success", json_string_value(v));
+
+       v = json_object_get(audit, "transactionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(
+               "00000000-0000-0000-0000-000000000000",
+               json_string_value(v));
+
+       v = json_object_get(audit, "objectCount");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(0, json_integer_value(v));
+
+       v = json_object_get(audit, "linkCount");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(0, json_integer_value(v));
+
+       v = json_object_get(audit, "partitionDN");
+       assert_non_null(v);
+       assert_true(json_is_null(v));
+
+       v = json_object_get(audit, "error");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(
+               "The operation completed successfully.",
+               json_string_value(v));
+
+       v = json_object_get(audit, "errorCode");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(0, json_integer_value(v));
+
+       v = json_object_get(audit, "sourceDsa");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(
+               "00000000-0000-0000-0000-000000000000",
+               json_string_value(v));
+
+       v = json_object_get(audit, "invocationId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(
+               "00000000-0000-0000-0000-000000000000",
+               json_string_value(v));
+
+       json_free(&json);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * unit test of replicated_update_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_replicated_update_json(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+       struct dsdb_extended_replicated_objects *ro = NULL;
+       struct repsFromTo1 *source_dsa = NULL;
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct ldb_dn *dn = NULL;
+       const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+
+       struct GUID source_dsa_obj_guid;
+       const char *const SOURCE_DSA = "7130cb06-2062-6a1b-409e-3514c26b1793";
+
+       struct GUID invocation_id;
+       const char *const INVOCATION_ID =
+               "7130cb06-2062-6a1b-409e-3514c26b1893";
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       json_t *o = NULL;
+       time_t before;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       ac = talloc_zero(ctx, struct audit_context);
+       GUID_from_string(TRANSACTION, &transaction_id);
+       ac->transaction_guid = transaction_id;
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       dn = ldb_dn_new(ctx, ldb, DN);
+       GUID_from_string(SOURCE_DSA, &source_dsa_obj_guid);
+       GUID_from_string(INVOCATION_ID, &invocation_id);
+       source_dsa = talloc_zero(ctx, struct repsFromTo1);
+       source_dsa->source_dsa_obj_guid = source_dsa_obj_guid;
+       source_dsa->source_dsa_invocation_id = invocation_id;
+
+       ro = talloc_zero(ctx, struct dsdb_extended_replicated_objects);
+       ro->source_dsa = source_dsa;
+       ro->num_objects = 808;
+       ro->linked_attributes_count = 2910;
+       ro->partition_dn = dn;
+       ro->error = WERR_NOT_SUPPORTED;
+
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->op.extended.data = ro;
+       req->operation = LDB_EXTENDED;
+
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_ERR_NO_SUCH_OBJECT;
+
+       before = time(NULL);
+       json = replicated_update_json(module, req, reply);
+       assert_int_equal(3, json_object_size(json.root));
+
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("replicatedUpdate", json_string_value(v));
+
+       v = json_object_get(json.root, "timestamp");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       check_timestamp(before, json_string_value(v));
+
+       audit = json_object_get(json.root, "replicatedUpdate");
+       assert_non_null(audit);
+       assert_true(json_is_object(audit));
+       assert_int_equal(11, json_object_size(audit));
+
+       o = json_object_get(audit, "version");
+       assert_non_null(o);
+       check_version(o, REPLICATION_MAJOR, REPLICATION_MINOR);
+
+       v = json_object_get(audit, "statusCode");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(LDB_ERR_NO_SUCH_OBJECT, json_integer_value(v));
+
+       v = json_object_get(audit, "status");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("No such object", json_string_value(v));
+
+       v = json_object_get(audit, "transactionId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(TRANSACTION, json_string_value(v));
+
+       v = json_object_get(audit, "objectCount");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(808, json_integer_value(v));
+
+       v = json_object_get(audit, "linkCount");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(2910, json_integer_value(v));
+
+       v = json_object_get(audit, "partitionDN");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(DN, json_string_value(v));
+
+       v = json_object_get(audit, "error");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(
+               "The request is not supported.",
+               json_string_value(v));
+
+       v = json_object_get(audit, "errorCode");
+       assert_non_null(v);
+       assert_true(json_is_integer(v));
+       assert_int_equal(W_ERROR_V(WERR_NOT_SUPPORTED), json_integer_value(v));
+
+       v = json_object_get(audit, "sourceDsa");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(SOURCE_DSA, json_string_value(v));
+
+       v = json_object_get(audit, "invocationId");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal(INVOCATION_ID, json_string_value(v));
+
+       json_free(&json);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * minimal unit test of operation_human_readable, that ensures that all the
+ * expected attributes and objects are in the json object.
+ */
+static void test_operation_hr_empty(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       char *line = NULL;
+       const char *rs = NULL;
+       regex_t regex;
+
+       int ret;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       ac = talloc_zero(ctx, struct audit_context);
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       req = talloc_zero(ctx, struct ldb_request);
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       line = operation_human_readable(ctx, module, req, reply);
+       assert_non_null(line);
+
+       /*
+        * We ignore the timestamp to make this test a little easier
+        * to write.
+        */
+       rs =    "\\[Search] at \\["
+               "[^[]*"
+               "\\] status \\[Success\\] remote host \\[Unknown\\]"
+               " SID \\[(NULL SID)\\] DN \\[(null)\\]";
+
+       ret = regcomp(&regex, rs, 0);
+       assert_int_equal(0, ret);
+
+       ret = regexec(&regex, line, 0, NULL, 0);
+       assert_int_equal(0, ret);
+
+       regfree(&regex);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * unit test of operation_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_operation_hr(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       struct tsocket_address *ts = NULL;
+
+       struct auth_session_info *sess = NULL;
+       struct security_token *token = NULL;
+       struct dom_sid sid;
+       const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       struct GUID session_id;
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct ldb_dn *dn = NULL;
+       const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+
+       struct ldb_message *msg = NULL;
+
+       char *line = NULL;
+       const char *rs = NULL;
+       regex_t regex;
+
+       int ret;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       ac = talloc_zero(ctx, struct audit_context);
+       GUID_from_string(TRANSACTION, &transaction_id);
+       ac->transaction_guid = transaction_id;
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts);
+       ldb_set_opaque(ldb, "remoteAddress", ts);
+
+       sess = talloc_zero(ctx, struct auth_session_info);
+       token = talloc_zero(ctx, struct security_token);
+       string_to_sid(&sid, SID);
+       token->num_sids = 1;
+       token->sids = &sid;
+       sess->security_token = token;
+       GUID_from_string(SESSION, &session_id);
+       sess->unique_session_token = session_id;
+       ldb_set_opaque(ldb, "sessionInfo", sess);
+
+       msg = talloc_zero(ctx, struct ldb_message);
+       dn = ldb_dn_new(ctx, ldb, DN);
+       msg->dn = dn;
+       ldb_msg_add_string(msg, "attribute", "the-value");
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       req->op.add.message = msg;
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       line = operation_human_readable(ctx, module, req, reply);
+       assert_non_null(line);
+
+       /*
+        * We ignore the timestamp to make this test a little easier
+        * to write.
+        */
+       rs =    "\\[Add\\] at \\["
+               "[^]]*"
+               "\\] status \\[Success\\] "
+               "remote host \\[ipv4:127.0.0.1:0\\] "
+               "SID \\[S-1-5-21-2470180966-3899876309-2637894779\\] "
+               "DN \\[dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG\\] "
+               "attributes \\[attribute \\[the-value\\]\\]";
+
+       ret = regcomp(&regex, rs, 0);
+       assert_int_equal(0, ret);
+
+       ret = regexec(&regex, line, 0, NULL, 0);
+       assert_int_equal(0, ret);
+
+       regfree(&regex);
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * unit test of operation_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ * In this case the operation is being performed in a system session.
+ */
+static void test_as_system_operation_hr(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       struct tsocket_address *ts = NULL;
+
+       struct auth_session_info *sess = NULL;
+       struct auth_session_info *sys_sess = NULL;
+       struct security_token *token = NULL;
+       struct security_token *sys_token = NULL;
+       struct dom_sid sid;
+       const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const SYS_SESSION = "7130cb06-2062-6a1b-409e-3514c26b1999";
+       struct GUID session_id;
+       struct GUID sys_session_id;
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct ldb_dn *dn = NULL;
+       const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+
+       struct ldb_message *msg = NULL;
+
+       char *line = NULL;
+       const char *rs = NULL;
+       regex_t regex;
+
+       int ret;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       ac = talloc_zero(ctx, struct audit_context);
+       GUID_from_string(TRANSACTION, &transaction_id);
+       ac->transaction_guid = transaction_id;
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts);
+       ldb_set_opaque(ldb, "remoteAddress", ts);
+
+       sess = talloc_zero(ctx, struct auth_session_info);
+       token = talloc_zero(ctx, struct security_token);
+       string_to_sid(&sid, SID);
+       token->num_sids = 1;
+       token->sids = &sid;
+       sess->security_token = token;
+       GUID_from_string(SESSION, &session_id);
+       sess->unique_session_token = session_id;
+       ldb_set_opaque(ldb, "networkSessionInfo", sess);
+
+       sys_sess = talloc_zero(ctx, struct auth_session_info);
+       sys_token = talloc_zero(ctx, struct security_token);
+       sys_token->num_sids = 1;
+       sys_token->sids = discard_const(&global_sid_System);
+       sys_sess->security_token = sys_token;
+       GUID_from_string(SYS_SESSION, &sys_session_id);
+       sess->unique_session_token = sys_session_id;
+       ldb_set_opaque(ldb, "sessionInfo", sys_sess);
+
+       msg = talloc_zero(ctx, struct ldb_message);
+       dn = ldb_dn_new(ctx, ldb, DN);
+       msg->dn = dn;
+       ldb_msg_add_string(msg, "attribute", "the-value");
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       req->op.add.message = msg;
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       line = operation_human_readable(ctx, module, req, reply);
+       assert_non_null(line);
+
+       /*
+        * We ignore the timestamp to make this test a little easier
+        * to write.
+        */
+       rs =    "\\[Add\\] at \\["
+               "[^]]*"
+               "\\] status \\[Success\\] "
+               "remote host \\[ipv4:127.0.0.1:0\\] "
+               "SID \\[S-1-5-21-2470180966-3899876309-2637894779\\] "
+               "DN \\[dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG\\] "
+               "attributes \\[attribute \\[the-value\\]\\]";
+
+       ret = regcomp(&regex, rs, 0);
+       assert_int_equal(0, ret);
+
+       ret = regexec(&regex, line, 0, NULL, 0);
+       assert_int_equal(0, ret);
+
+       regfree(&regex);
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * minimal unit test of password_change_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_password_change_hr_empty(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       char *line = NULL;
+       const char *rs = NULL;
+       regex_t regex;
+       int ret;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       ac = talloc_zero(ctx, struct audit_context);
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       req = talloc_zero(ctx, struct ldb_request);
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       line = password_change_human_readable(ctx, module, req, reply);
+       assert_non_null(line);
+
+       /*
+        * We ignore the timestamp to make this test a little easier
+        * to write.
+        */
+       rs =    "\\[Reset] at \\["
+               "[^[]*"
+               "\\] status \\[Success\\] remote host \\[Unknown\\]"
+               " SID \\[(NULL SID)\\] DN \\[(null)\\]";
+
+       ret = regcomp(&regex, rs, 0);
+       assert_int_equal(0, ret);
+
+       ret = regexec(&regex, line, 0, NULL, 0);
+       assert_int_equal(0, ret);
+
+       regfree(&regex);
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * minimal unit test of password_change_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_password_change_hr(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+
+       struct tsocket_address *ts = NULL;
+
+       struct auth_session_info *sess = NULL;
+       struct security_token *token = NULL;
+       struct dom_sid sid;
+       const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       struct GUID session_id;
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct ldb_dn *dn = NULL;
+       const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+
+       struct ldb_message *msg = NULL;
+
+       char *line = NULL;
+       const char *rs = NULL;
+       regex_t regex;
+       int ret;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       ac = talloc_zero(ctx, struct audit_context);
+       GUID_from_string(TRANSACTION, &transaction_id);
+       ac->transaction_guid = transaction_id;
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts);
+       ldb_set_opaque(ldb, "remoteAddress", ts);
+
+       sess = talloc_zero(ctx, struct auth_session_info);
+       token = talloc_zero(ctx, struct security_token);
+       string_to_sid(&sid, SID);
+       token->num_sids = 1;
+       token->sids = &sid;
+       sess->security_token = token;
+       GUID_from_string(SESSION, &session_id);
+       sess->unique_session_token = session_id;
+       ldb_set_opaque(ldb, "sessionInfo", sess);
+
+       msg = talloc_zero(ctx, struct ldb_message);
+       dn = ldb_dn_new(ctx, ldb, DN);
+       msg->dn = dn;
+       ldb_msg_add_string(msg, "planTextPassword", "super-secret");
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       req->op.add.message = msg;
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       line = password_change_human_readable(ctx, module, req, reply);
+       assert_non_null(line);
+
+       /*
+        * We ignore the timestamp to make this test a little easier
+        * to write.
+        */
+       rs =    "\\[Reset\\] at \\["
+               "[^[]*"
+               "\\] status \\[Success\\] "
+               "remote host \\[ipv4:127.0.0.1:0\\] "
+               "SID \\[S-1-5-21-2470180966-3899876309-2637894779\\] "
+               "DN \\[dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG\\]";
+
+       ret = regcomp(&regex, rs, 0);
+       assert_int_equal(0, ret);
+
+       ret = regexec(&regex, line, 0, NULL, 0);
+       assert_int_equal(0, ret);
+
+       regfree(&regex);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * minimal unit test of transaction_json, that ensures that all the expected
+ * attributes and objects are in the json object.
+ */
+static void test_transaction_hr(void **state)
+{
+
+       struct GUID guid;
+       const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       char *line = NULL;
+       const char *rs = NULL;
+       regex_t regex;
+       int ret;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       GUID_from_string(GUID, &guid);
+
+       line = transaction_human_readable(ctx, "delete");
+       assert_non_null(line);
+
+       /*
+        * We ignore the timestamp to make this test a little easier
+        * to write.
+        */
+       rs = "\\[delete] at \\[[^[]*\\]";
+
+       ret = regcomp(&regex, rs, 0);
+       assert_int_equal(0, ret);
+
+       ret = regexec(&regex, line, 0, NULL, 0);
+       assert_int_equal(0, ret);
+
+       regfree(&regex);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * minimal unit test of commit_failure_hr, that ensures
+ * that all the expected conten is in the log entry.
+ */
+static void test_commit_failure_hr(void **state)
+{
+
+       struct GUID guid;
+       const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       char *line = NULL;
+       const char *rs = NULL;
+       regex_t regex;
+       int ret;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       GUID_from_string(GUID, &guid);
+
+       line = commit_failure_human_readable(
+               ctx,
+               "commit",
+               LDB_ERR_OPERATIONS_ERROR,
+               "because");
+
+       assert_non_null(line);
+
+       /*
+        * We ignore the timestamp to make this test a little easier
+        * to write.
+        */
+       rs = "\\[commit\\] at \\[[^[]*\\] status \\[1\\] reason \\[because\\]";
+
+       ret = regcomp(&regex, rs, 0);
+       assert_int_equal(0, ret);
+
+       ret = regexec(&regex, line, 0, NULL, 0);
+       assert_int_equal(0, ret);
+
+       regfree(&regex);
+       TALLOC_FREE(ctx);
+}
+
+static void test_add_transaction_id(void **state)
+{
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct audit_context *ac = NULL;
+       struct GUID guid;
+       const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       struct ldb_control * control = NULL;
+       int status;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ac = talloc_zero(ctx, struct audit_context);
+       GUID_from_string(GUID, &guid);
+       ac->transaction_guid = guid;
+
+       module = talloc_zero(ctx, struct ldb_module);
+       ldb_module_set_private(module, ac);
+
+       req = talloc_zero(ctx, struct ldb_request);
+
+       status = add_transaction_id(module, req);
+       assert_int_equal(LDB_SUCCESS, status);
+
+       control = ldb_request_get_control(
+               req,
+               DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID);
+       assert_non_null(control);
+       assert_memory_equal(
+               &ac->transaction_guid,
+               control->data,
+               sizeof(struct GUID));
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_log_attributes(void **state)
+{
+       struct ldb_message *msg = NULL;
+
+       char *buf = NULL;
+       char *str = NULL;
+       char lv[MAX_LENGTH+2];
+       char ex[MAX_LENGTH+80];
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+
+       /*
+        * Test an empty message
+        * Should get empty attributes representation.
+        */
+       buf = talloc_zero(ctx, char);
+       msg = talloc_zero(ctx, struct ldb_message);
+
+       str = log_attributes(ctx, buf, LDB_ADD, msg);
+       assert_string_equal("", str);
+
+       TALLOC_FREE(str);
+       TALLOC_FREE(msg);
+
+       /*
+        * Test a message with a single secret attribute
+        */
+       buf = talloc_zero(ctx, char);
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb_msg_add_string(msg, "clearTextPassword", "secret");
+
+       str = log_attributes(ctx, buf, LDB_ADD, msg);
+       assert_string_equal(
+               "clearTextPassword [REDACTED SECRET ATTRIBUTE]",
+               str);
+       TALLOC_FREE(str);
+       /*
+        * Test as a modify message, should add an action
+        * action will be unknown as there are no ACL's set
+        */
+       buf = talloc_zero(ctx, char);
+       str = log_attributes(ctx, buf, LDB_MODIFY, msg);
+       assert_string_equal(
+               "unknown: clearTextPassword [REDACTED SECRET ATTRIBUTE]",
+               str);
+
+       TALLOC_FREE(str);
+       TALLOC_FREE(msg);
+
+       /*
+        * Test a message with a single attribute, single valued attribute
+        */
+       buf = talloc_zero(ctx, char);
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb_msg_add_string(msg, "attribute", "value");
+
+       str = log_attributes(ctx, buf, LDB_ADD, msg);
+       assert_string_equal(
+               "attribute [value]",
+               str);
+
+       TALLOC_FREE(str);
+       TALLOC_FREE(msg);
+
+       /*
+        * Test a message with a single attribute, single valued attribute
+        * And as a modify
+        */
+       buf = talloc_zero(ctx, char);
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb_msg_add_string(msg, "attribute", "value");
+
+       str = log_attributes(ctx, buf, LDB_MODIFY, msg);
+       assert_string_equal(
+               "unknown: attribute [value]",
+               str);
+
+       TALLOC_FREE(str);
+       TALLOC_FREE(msg);
+
+       /*
+        * Test a message with multiple attributes and a multi-valued attribute
+        *
+        */
+       buf = talloc_zero(ctx, char);
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb_msg_add_string(msg, "attribute01", "value01");
+       ldb_msg_add_string(msg, "attribute02", "value02");
+       ldb_msg_add_string(msg, "attribute02", "value03");
+
+       str = log_attributes(ctx, buf, LDB_MODIFY, msg);
+       assert_string_equal(
+               "unknown: attribute01 [value01] "
+               "unknown: attribute02 [value02] [value03]",
+               str);
+
+       TALLOC_FREE(str);
+       TALLOC_FREE(msg);
+
+       /*
+        * Test a message with a single attribute, single valued attribute
+        * with a non printable character. Should be base64 encoded
+        */
+       buf = talloc_zero(ctx, char);
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb_msg_add_string(msg, "attribute", "value\n");
+
+       str = log_attributes(ctx, buf, LDB_ADD, msg);
+       assert_string_equal("attribute {dmFsdWUK}", str);
+
+       TALLOC_FREE(str);
+       TALLOC_FREE(msg);
+
+       /*
+        * Test a message with a single valued attribute
+        * with more than MAX_LENGTH characters, should be truncated with
+        * trailing ...
+        */
+       buf = talloc_zero(ctx, char);
+       msg = talloc_zero(ctx, struct ldb_message);
+       memset(lv, '\0', sizeof(lv));
+       memset(lv, 'x', MAX_LENGTH+1);
+       ldb_msg_add_string(msg, "attribute", lv);
+
+       str = log_attributes(ctx, buf, LDB_ADD, msg);
+       snprintf(ex, sizeof(ex), "attribute [%.*s...]", MAX_LENGTH, lv);
+       assert_string_equal(ex, str);
+
+       TALLOC_FREE(str);
+       TALLOC_FREE(msg);
+
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * minimal unit test of replicated_update_human_readable
+ */
+static void test_replicated_update_hr_empty(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+       struct dsdb_extended_replicated_objects *ro = NULL;
+       struct repsFromTo1 *source_dsa = NULL;
+
+       const char* line = NULL;
+       const char *rs = NULL;
+       regex_t regex;
+       int ret;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       ac = talloc_zero(ctx, struct audit_context);
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       source_dsa = talloc_zero(ctx, struct repsFromTo1);
+       ro = talloc_zero(ctx, struct dsdb_extended_replicated_objects);
+       ro->source_dsa = source_dsa;
+       req = talloc_zero(ctx, struct ldb_request);
+       req->op.extended.data = ro;
+       req->operation = LDB_EXTENDED;
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_SUCCESS;
+
+       line = replicated_update_human_readable(ctx, module, req, reply);
+       assert_non_null(line);
+       /*
+        * We ignore the timestamp to make this test a little easier
+        * to write.
+        */
+       rs =    "at \\[[^[]*\\] "
+               "status \\[Success\\] "
+               "error \\[The operation completed successfully.\\] "
+               "partition \\[(null)\\] objects \\[0\\] links \\[0\\] "
+               "object \\[00000000-0000-0000-0000-000000000000\\] "
+               "invocation \\[00000000-0000-0000-0000-000000000000\\]";
+
+       ret = regcomp(&regex, rs, 0);
+       assert_int_equal(0, ret);
+
+       ret = regexec(&regex, line, 0, NULL, 0);
+       assert_int_equal(0, ret);
+
+       regfree(&regex);
+       TALLOC_FREE(ctx);
+
+}
+
+/*
+ * unit test of replicated_update_human_readable
+ */
+static void test_replicated_update_hr(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+       struct ldb_reply *reply = NULL;
+       struct audit_context *ac = NULL;
+       struct dsdb_extended_replicated_objects *ro = NULL;
+       struct repsFromTo1 *source_dsa = NULL;
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct ldb_dn *dn = NULL;
+       const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+
+       struct GUID source_dsa_obj_guid;
+       const char *const SOURCE_DSA = "7130cb06-2062-6a1b-409e-3514c26b1793";
+
+       struct GUID invocation_id;
+       const char *const INVOCATION_ID =
+               "7130cb06-2062-6a1b-409e-3514c26b1893";
+
+       const char* line = NULL;
+       const char *rs = NULL;
+       regex_t regex;
+       int ret;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       ac = talloc_zero(ctx, struct audit_context);
+       GUID_from_string(TRANSACTION, &transaction_id);
+       ac->transaction_guid = transaction_id;
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+       ldb_module_set_private(module, ac);
+
+       dn = ldb_dn_new(ctx, ldb, DN);
+       GUID_from_string(SOURCE_DSA, &source_dsa_obj_guid);
+       GUID_from_string(INVOCATION_ID, &invocation_id);
+       source_dsa = talloc_zero(ctx, struct repsFromTo1);
+       source_dsa->source_dsa_obj_guid = source_dsa_obj_guid;
+       source_dsa->source_dsa_invocation_id = invocation_id;
+
+       ro = talloc_zero(ctx, struct dsdb_extended_replicated_objects);
+       ro->source_dsa = source_dsa;
+       ro->num_objects = 808;
+       ro->linked_attributes_count = 2910;
+       ro->partition_dn = dn;
+       ro->error = WERR_NOT_SUPPORTED;
+
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->op.extended.data = ro;
+       req->operation = LDB_EXTENDED;
+
+       reply = talloc_zero(ctx, struct ldb_reply);
+       reply->error = LDB_ERR_NO_SUCH_OBJECT;
+
+       line = replicated_update_human_readable(ctx, module, req, reply);
+       assert_non_null(line);
+
+       /*
+        * We ignore the timestamp to make this test a little easier
+        * to write.
+        */
+       rs =    "at \\[[^[]*\\] "
+               "status \\[No such object\\] "
+               "error \\[The request is not supported.\\] "
+               "partition \\[dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG\\] "
+               "objects \\[808\\] links \\[2910\\] "
+               "object \\[7130cb06-2062-6a1b-409e-3514c26b1793\\] "
+               "invocation \\[7130cb06-2062-6a1b-409e-3514c26b1893\\]";
+
+       ret = regcomp(&regex, rs, 0);
+       assert_int_equal(0, ret);
+
+       ret = regexec(&regex, line, 0, NULL, 0);
+       assert_int_equal(0, ret);
+
+       regfree(&regex);
+       TALLOC_FREE(ctx);
+}
+
+int main(void) {
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test(test_has_password_changed),
+               cmocka_unit_test(test_get_password_action),
+               cmocka_unit_test(test_operation_json_empty),
+               cmocka_unit_test(test_operation_json),
+               cmocka_unit_test(test_as_system_operation_json),
+               cmocka_unit_test(test_password_change_json_empty),
+               cmocka_unit_test(test_password_change_json),
+               cmocka_unit_test(test_transaction_json),
+               cmocka_unit_test(test_commit_failure_json),
+               cmocka_unit_test(test_replicated_update_json_empty),
+               cmocka_unit_test(test_replicated_update_json),
+               cmocka_unit_test(test_add_transaction_id),
+               cmocka_unit_test(test_operation_hr_empty),
+               cmocka_unit_test(test_operation_hr),
+               cmocka_unit_test(test_as_system_operation_hr),
+               cmocka_unit_test(test_password_change_hr_empty),
+               cmocka_unit_test(test_password_change_hr),
+               cmocka_unit_test(test_transaction_hr),
+               cmocka_unit_test(test_commit_failure_hr),
+               cmocka_unit_test(test_log_attributes),
+               cmocka_unit_test(test_replicated_update_hr_empty),
+               cmocka_unit_test(test_replicated_update_hr),
+       };
+
+       cmocka_set_message_output(CM_OUTPUT_SUBUNIT);
+       return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c b/source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c
new file mode 100644 (file)
index 0000000..7f7e1b5
--- /dev/null
@@ -0,0 +1,1260 @@
+/*
+   Unit tests for the dsdb audit logging utility code code in audit_util.c
+
+   Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <unistd.h>
+#include <cmocka.h>
+
+#include "../audit_util.c"
+
+#include "lib/ldb/include/ldb_private.h"
+
+static void test_dsdb_audit_add_ldb_value(void **state)
+{
+       struct json_object object;
+       struct json_object array;
+       struct ldb_val val = data_blob_null;
+       struct json_t *el  = NULL;
+       struct json_t *atr = NULL;
+       char* base64 = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+       /*
+        * Test a non array object
+        */
+       object = json_new_object();
+       assert_false(json_is_invalid(&object));
+       dsdb_audit_add_ldb_value(&object, val);
+       assert_true(json_is_invalid(&object));
+       json_free(&object);
+
+       array = json_new_array();
+       /*
+        * Test a data_blob_null, should encode as a JSON null value.
+        */
+       val = data_blob_null;
+       dsdb_audit_add_ldb_value(&array, val);
+       el = json_array_get(array.root, 0);
+       assert_true(json_is_null(el));
+
+       /*
+        * Test a +ve length but a null data ptr, should encode as a null.
+        */
+       val = data_blob_null;
+       val.length = 1;
+       dsdb_audit_add_ldb_value(&array, val);
+       el = json_array_get(array.root, 1);
+       assert_true(json_is_null(el));
+
+       /*
+        * Test a zero length but a non null data ptr, should encode as a null.
+        */
+       val = data_blob_null;
+       val.data = discard_const("Data on the stack");
+       dsdb_audit_add_ldb_value(&array, val);
+       el = json_array_get(array.root, 2);
+       assert_true(json_is_null(el));
+
+       /*
+        * Test a printable value.
+        * value should not be encoded
+        * truncated and base64 should be missing
+        */
+       val = data_blob_string_const("A value of interest");
+       dsdb_audit_add_ldb_value(&array, val);
+       el = json_array_get(array.root, 3);
+       assert_true(json_is_object(el));
+       atr = json_object_get(el, "value");
+       assert_true(json_is_string(atr));
+       assert_string_equal("A value of interest", json_string_value(atr));
+       assert_null(json_object_get(el, "truncated"));
+       assert_null(json_object_get(el, "base64"));
+
+       /*
+        * Test non printable value, should be base64 encoded.
+        * truncated should be missing and base64 should be set.
+        */
+       val = data_blob_string_const("A value of interest\n");
+       dsdb_audit_add_ldb_value(&array, val);
+       el = json_array_get(array.root, 4);
+       assert_true(json_is_object(el));
+       atr = json_object_get(el, "value");
+       assert_true(json_is_string(atr));
+       assert_string_equal(
+               "QSB2YWx1ZSBvZiBpbnRlcmVzdAo=",
+               json_string_value(atr));
+       atr = json_object_get(el, "base64");
+       assert_true(json_is_boolean(atr));
+       assert_true(json_boolean(atr));
+       assert_null(json_object_get(el, "truncated"));
+
+       /*
+        * test a printable value exactly max bytes long
+        * should not be truncated or encoded.
+        */
+       val = data_blob_null;
+       val.length = MAX_LENGTH;
+       val.data = (unsigned char *)generate_random_str_list(
+               ctx,
+               MAX_LENGTH,
+               "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+               "1234567890!@#$%^&*()");
+
+       dsdb_audit_add_ldb_value(&array, val);
+
+       el = json_array_get(array.root, 5);
+       assert_true(json_is_object(el));
+       atr = json_object_get(el, "value");
+       assert_true(json_is_string(atr));
+       assert_int_equal(MAX_LENGTH, strlen(json_string_value(atr)));
+       assert_memory_equal(val.data, json_string_value(atr), MAX_LENGTH);
+
+       assert_null(json_object_get(el, "base64"));
+       assert_null(json_object_get(el, "truncated"));
+
+
+       /*
+        * test a printable value exactly max + 1 bytes long
+        * should be truncated and not encoded.
+        */
+       val = data_blob_null;
+       val.length = MAX_LENGTH + 1;
+       val.data = (unsigned char *)generate_random_str_list(
+               ctx,
+               MAX_LENGTH + 1,
+               "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+               "1234567890!@#$%^&*()");
+
+       dsdb_audit_add_ldb_value(&array, val);
+
+       el = json_array_get(array.root, 6);
+       assert_true(json_is_object(el));
+       atr = json_object_get(el, "value");
+       assert_true(json_is_string(atr));
+       assert_int_equal(MAX_LENGTH, strlen(json_string_value(atr)));
+       assert_memory_equal(val.data, json_string_value(atr), MAX_LENGTH);
+
+       atr = json_object_get(el, "truncated");
+       assert_true(json_is_boolean(atr));
+       assert_true(json_boolean(atr));
+
+       assert_null(json_object_get(el, "base64"));
+
+       TALLOC_FREE(val.data);
+
+       /*
+        * test a non-printable value exactly max bytes long
+        * should not be truncated but should be encoded.
+        */
+       val = data_blob_null;
+       val.length = MAX_LENGTH;
+       val.data = (unsigned char *)generate_random_str_list(
+               ctx,
+               MAX_LENGTH,
+               "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+               "1234567890!@#$%^&*()");
+
+       val.data[0] = 0x03;
+       dsdb_audit_add_ldb_value(&array, val);
+       base64 = ldb_base64_encode(ctx, (char*) val.data, MAX_LENGTH);
+
+       el = json_array_get(array.root, 7);
+       assert_true(json_is_object(el));
+       atr = json_object_get(el, "value");
+       assert_true(json_is_string(atr));
+       assert_int_equal(strlen(base64), strlen(json_string_value(atr)));
+       assert_string_equal(base64, json_string_value(atr));
+
+       atr = json_object_get(el, "base64");
+       assert_true(json_is_boolean(atr));
+       assert_true(json_boolean(atr));
+
+       assert_null(json_object_get(el, "truncated"));
+       TALLOC_FREE(base64);
+       TALLOC_FREE(val.data);
+
+       /*
+        * test a non-printable value exactly max + 1 bytes long
+        * should be truncated and encoded.
+        */
+       val = data_blob_null;
+       val.length = MAX_LENGTH + 1;
+       val.data = (unsigned char *)generate_random_str_list(
+               ctx,
+               MAX_LENGTH + 1,
+               "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+               "1234567890!@#$%^&*()");
+
+       val.data[0] = 0x03;
+       dsdb_audit_add_ldb_value(&array, val);
+       /*
+        * The data is truncated before it is base 64 encoded
+        */
+       base64 = ldb_base64_encode(ctx, (char*) val.data, MAX_LENGTH);
+
+       el = json_array_get(array.root, 8);
+       assert_true(json_is_object(el));
+       atr = json_object_get(el, "value");
+       assert_true(json_is_string(atr));
+       assert_int_equal(strlen(base64), strlen(json_string_value(atr)));
+       assert_string_equal(base64, json_string_value(atr));
+
+       atr = json_object_get(el, "base64");
+       assert_true(json_is_boolean(atr));
+       assert_true(json_boolean(atr));
+
+       atr = json_object_get(el, "truncated");
+       assert_true(json_is_boolean(atr));
+       assert_true(json_boolean(atr));
+
+       TALLOC_FREE(base64);
+       TALLOC_FREE(val.data);
+
+       json_free(&array);
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_attributes_json(void **state)
+{
+       struct ldb_message *msg = NULL;
+
+       struct json_object o;
+       json_t *a = NULL;
+       json_t *v = NULL;
+       json_t *x = NULL;
+       json_t *y = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+
+       /*
+        * Test an empty message
+        * Should get an empty attributes object
+        */
+       msg = talloc_zero(ctx, struct ldb_message);
+
+       o = dsdb_audit_attributes_json(LDB_ADD, msg);
+       assert_true(json_is_object(o.root));
+       assert_int_equal(0, json_object_size(o.root));
+       json_free(&o);
+
+       o = dsdb_audit_attributes_json(LDB_MODIFY, msg);
+       assert_true(json_is_object(o.root));
+       assert_int_equal(0, json_object_size(o.root));
+       json_free(&o);
+
+       /*
+        * Test a message with a single secret attribute
+        * should only have that object and it should have no value
+        * attribute and redacted should be set.
+        */
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb_msg_add_string(msg, "clearTextPassword", "secret");
+
+       o = dsdb_audit_attributes_json(LDB_ADD, msg);
+       assert_true(json_is_object(o.root));
+       assert_int_equal(1, json_object_size(o.root));
+
+       a = json_object_get(o.root, "clearTextPassword");
+       assert_int_equal(1, json_object_size(a));
+
+       v = json_object_get(a, "actions");
+       assert_true(json_is_array(v));
+       assert_int_equal(1, json_array_size(v));
+
+       a = json_array_get(v, 0);
+       v = json_object_get(a, "redacted");
+       assert_true(json_is_boolean(v));
+       assert_true(json_boolean(v));
+
+       json_free(&o);
+
+       /*
+        * Test as a modify message, should add an action attribute
+        */
+       o = dsdb_audit_attributes_json(LDB_MODIFY, msg);
+       assert_true(json_is_object(o.root));
+       assert_int_equal(1, json_object_size(o.root));
+
+       a = json_object_get(o.root, "clearTextPassword");
+       assert_true(json_is_object(a));
+       assert_int_equal(1, json_object_size(a));
+
+       v = json_object_get(a, "actions");
+       assert_true(json_is_array(v));
+       assert_int_equal(1, json_array_size(v));
+
+       a = json_array_get(v, 0);
+       v = json_object_get(a, "redacted");
+       assert_true(json_is_boolean(v));
+       assert_true(json_boolean(v));
+
+       v = json_object_get(a, "action");
+       assert_true(json_is_string(v));
+       assert_string_equal("unknown", json_string_value(v));
+
+       json_free(&o);
+       TALLOC_FREE(msg);
+
+       /*
+        * Test a message with a single attribute, single valued attribute
+        */
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb_msg_add_string(msg, "attribute", "value");
+
+       o = dsdb_audit_attributes_json(LDB_ADD, msg);
+       assert_true(json_is_object(o.root));
+       assert_int_equal(1, json_object_size(o.root));
+
+       a = json_object_get(o.root, "attribute");
+       assert_true(json_is_object(a));
+       assert_int_equal(1, json_object_size(a));
+
+       v = json_object_get(a, "actions");
+       assert_true(json_is_array(v));
+       assert_int_equal(1, json_array_size(v));
+
+       x = json_array_get(v, 0);
+       assert_int_equal(2, json_object_size(x));
+       y = json_object_get(x, "action");
+       assert_string_equal("add", json_string_value(y));
+
+       y = json_object_get(x, "values");
+       assert_true(json_is_array(y));
+       assert_int_equal(1, json_array_size(y));
+
+       x = json_array_get(y, 0);
+       assert_true(json_is_object(x));
+       assert_int_equal(1, json_object_size(x));
+       y = json_object_get(x, "value");
+       assert_string_equal("value", json_string_value(y));
+
+       json_free(&o);
+       TALLOC_FREE(msg);
+
+       /*
+        * Test a message with a single attribute, single valued attribute
+        * And as a modify
+        */
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb_msg_add_string(msg, "attribute", "value");
+
+       o = dsdb_audit_attributes_json(LDB_MODIFY, msg);
+       assert_true(json_is_object(o.root));
+       assert_int_equal(1, json_object_size(o.root));
+
+       a = json_object_get(o.root, "attribute");
+       assert_true(json_is_object(a));
+       assert_int_equal(1, json_object_size(a));
+
+       v = json_object_get(a, "actions");
+       assert_true(json_is_array(v));
+       assert_int_equal(1, json_array_size(v));
+
+       x = json_array_get(v, 0);
+       assert_int_equal(2, json_object_size(x));
+       y = json_object_get(x, "action");
+       assert_string_equal("unknown", json_string_value(y));
+
+       y = json_object_get(x, "values");
+       assert_true(json_is_array(y));
+       assert_int_equal(1, json_array_size(y));
+
+       x = json_array_get(y, 0);
+       assert_true(json_is_object(x));
+       assert_int_equal(1, json_object_size(x));
+       y = json_object_get(x, "value");
+       assert_string_equal("value", json_string_value(y));
+
+       json_free(&o);
+       TALLOC_FREE(msg);
+
+       /*
+        * Test a message with a multivalues attributres
+        */
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb_msg_add_string(msg, "attribute01", "value01");
+       ldb_msg_add_string(msg, "attribute02", "value02");
+       ldb_msg_add_string(msg, "attribute02", "value03");
+
+       o = dsdb_audit_attributes_json(LDB_ADD, msg);
+       assert_true(json_is_object(o.root));
+       assert_int_equal(2, json_object_size(o.root));
+
+       a = json_object_get(o.root, "attribute01");
+       assert_true(json_is_object(a));
+       assert_int_equal(1, json_object_size(a));
+
+       v = json_object_get(a, "actions");
+       assert_true(json_is_array(v));
+       assert_int_equal(1, json_array_size(v));
+
+       x = json_array_get(v, 0);
+       assert_int_equal(2, json_object_size(x));
+       y = json_object_get(x, "action");
+       assert_string_equal("add", json_string_value(y));
+
+       y = json_object_get(x, "values");
+       assert_true(json_is_array(y));
+       assert_int_equal(1, json_array_size(y));
+
+       x = json_array_get(y, 0);
+       assert_true(json_is_object(x));
+       assert_int_equal(1, json_object_size(x));
+       y = json_object_get(x, "value");
+       assert_string_equal("value01", json_string_value(y));
+
+       a = json_object_get(o.root, "attribute02");
+       assert_true(json_is_object(a));
+       assert_int_equal(1, json_object_size(a));
+
+       v = json_object_get(a, "actions");
+       assert_true(json_is_array(v));
+       assert_int_equal(1, json_array_size(v));
+
+       x = json_array_get(v, 0);
+       assert_int_equal(2, json_object_size(x));
+       y = json_object_get(x, "action");
+       assert_string_equal("add", json_string_value(y));
+
+       y = json_object_get(x, "values");
+       assert_true(json_is_array(y));
+       assert_int_equal(2, json_array_size(y));
+
+       x = json_array_get(y, 0);
+       assert_true(json_is_object(x));
+       assert_int_equal(1, json_object_size(x));
+       v = json_object_get(x, "value");
+       assert_string_equal("value02", json_string_value(v));
+
+       x = json_array_get(y, 1);
+       assert_true(json_is_object(x));
+       assert_int_equal(1, json_object_size(x));
+       v = json_object_get(x, "value");
+       assert_string_equal("value03", json_string_value(v));
+
+       json_free(&o);
+       TALLOC_FREE(msg);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_get_remote_address(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       const struct tsocket_address *ts = NULL;
+       struct tsocket_address *in = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       /*
+        * Test a freshly initialized ldb
+        * should return NULL
+        */
+       ldb = talloc_zero(ctx, struct ldb_context);
+       ts = dsdb_audit_get_remote_address(ldb);
+       assert_null(ts);
+
+       /*
+        * opaque set to null, should return NULL
+        */
+       ldb_set_opaque(ldb, "remoteAddress", NULL);
+       ts = dsdb_audit_get_remote_address(ldb);
+       assert_null(ts);
+
+       /*
+        * Ensure that the value set is returned
+        */
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &in);
+       ldb_set_opaque(ldb, "remoteAddress", in);
+       ts = dsdb_audit_get_remote_address(ldb);
+       assert_non_null(ts);
+       assert_ptr_equal(in, ts);
+
+       TALLOC_FREE(ldb);
+       TALLOC_FREE(ctx);
+
+}
+
+static void test_dsdb_audit_get_ldb_error_string(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module *module = NULL;
+       const char *s = NULL;
+       const char * const text = "Custom reason";
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+
+       /*
+        * No ldb error string set should get the default error description for
+        * the status code
+        */
+       s = dsdb_audit_get_ldb_error_string(module, LDB_ERR_OPERATIONS_ERROR);
+       assert_string_equal("Operations error", s);
+
+       /*
+        * Set the error string that should now be returned instead of the
+        * default description.
+        */
+       ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, text);
+       s = dsdb_audit_get_ldb_error_string(module, LDB_ERR_OPERATIONS_ERROR);
+       /*
+        * Only test the start of the string as ldb_error adds location data.
+        */
+       assert_int_equal(0, strncmp(text, s, strlen(text)));
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_get_user_sid(void **state)
+{
+       struct ldb_context *ldb        = NULL;
+       struct ldb_module *module      = NULL;
+       const struct dom_sid *sid      = NULL;
+       struct auth_session_info *sess = NULL;
+       struct security_token *token   = NULL;
+       struct dom_sid sids[2];
+       const char * const SID0 = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SID1 = "S-1-5-21-4284042908-2889457889-3672286761";
+       char sid_buf[DOM_SID_STR_BUFLEN];
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+
+       /*
+        * Freshly initialised structures, will be no session data
+        * so expect NULL
+        */
+       sid = dsdb_audit_get_user_sid(module);
+       assert_null(sid);
+
+       /*
+        * Now add a NULL session info
+        */
+       ldb_set_opaque(ldb, "sessionInfo", NULL);
+       sid = dsdb_audit_get_user_sid(module);
+       assert_null(sid);
+
+       /*
+        * Now add a session info with no user sid
+        */
+       sess = talloc_zero(ctx, struct auth_session_info);
+       ldb_set_opaque(ldb, "sessionInfo", sess);
+       sid = dsdb_audit_get_user_sid(module);
+       assert_null(sid);
+
+       /*
+        * Now add an empty security token.
+        */
+       token = talloc_zero(ctx, struct security_token);
+       sess->security_token = token;
+       sid = dsdb_audit_get_user_sid(module);
+       assert_null(sid);
+
+       /*
+        * Add a single SID
+        */
+       string_to_sid(&sids[0], SID0);
+       token->num_sids = 1;
+       token->sids = sids;
+       sid = dsdb_audit_get_user_sid(module);
+       assert_non_null(sid);
+       dom_sid_string_buf(sid, sid_buf, sizeof(sid_buf));
+       assert_string_equal(SID0, sid_buf);
+
+       /*
+        * Add a second SID, should still use the first SID
+        */
+       string_to_sid(&sids[1], SID1);
+       token->num_sids = 2;
+       sid = dsdb_audit_get_user_sid(module);
+       assert_non_null(sid);
+       dom_sid_string_buf(sid, sid_buf, sizeof(sid_buf));
+       assert_string_equal(SID0, sid_buf);
+
+
+       /*
+        * Now test a null sid in the first position
+        */
+       token->num_sids = 1;
+       token->sids = NULL;
+       sid = dsdb_audit_get_user_sid(module);
+       assert_null(sid);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_get_actual_sid(void **state)
+{
+       struct ldb_context *ldb        = NULL;
+       const struct dom_sid *sid      = NULL;
+       struct auth_session_info *sess = NULL;
+       struct security_token *token   = NULL;
+       struct dom_sid sids[2];
+       const char * const SID0 = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SID1 = "S-1-5-21-4284042908-2889457889-3672286761";
+       char sid_buf[DOM_SID_STR_BUFLEN];
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       /*
+        * Freshly initialised structures, will be no session data
+        * so expect NULL
+        */
+       sid = dsdb_audit_get_actual_sid(ldb);
+       assert_null(sid);
+
+       /*
+        * Now add a NULL session info
+        */
+       ldb_set_opaque(ldb, "networkSessionInfo", NULL);
+       sid = dsdb_audit_get_actual_sid(ldb);
+       assert_null(sid);
+
+       /*
+        * Now add a session info with no user sid
+        */
+       sess = talloc_zero(ctx, struct auth_session_info);
+       ldb_set_opaque(ldb, "networkSessionInfo", sess);
+       sid = dsdb_audit_get_actual_sid(ldb);
+       assert_null(sid);
+
+       /*
+        * Now add an empty security token.
+        */
+       token = talloc_zero(ctx, struct security_token);
+       sess->security_token = token;
+       sid = dsdb_audit_get_actual_sid(ldb);
+       assert_null(sid);
+
+       /*
+        * Add a single SID
+        */
+       string_to_sid(&sids[0], SID0);
+       token->num_sids = 1;
+       token->sids = sids;
+       sid = dsdb_audit_get_actual_sid(ldb);
+       assert_non_null(sid);
+       dom_sid_string_buf(sid, sid_buf, sizeof(sid_buf));
+       assert_string_equal(SID0, sid_buf);
+
+       /*
+        * Add a second SID, should still use the first SID
+        */
+       string_to_sid(&sids[1], SID1);
+       token->num_sids = 2;
+       sid = dsdb_audit_get_actual_sid(ldb);
+       assert_non_null(sid);
+       dom_sid_string_buf(sid, sid_buf, sizeof(sid_buf));
+       assert_string_equal(SID0, sid_buf);
+
+
+       /*
+        * Now test a null sid in the first position
+        */
+       token->num_sids = 1;
+       token->sids = NULL;
+       sid = dsdb_audit_get_actual_sid(ldb);
+       assert_null(sid);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_is_system_session(void **state)
+{
+       struct ldb_context *ldb        = NULL;
+       struct ldb_module *module      = NULL;
+       const struct dom_sid *sid      = NULL;
+       struct auth_session_info *sess = NULL;
+       struct security_token *token   = NULL;
+       struct dom_sid sids[2];
+       const char * const SID0 = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SID1 = "S-1-5-21-4284042908-2889457889-3672286761";
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+
+       /*
+        * Freshly initialised structures, will be no session data
+        * so expect NULL
+        */
+       assert_false(dsdb_audit_is_system_session(module));
+
+       /*
+        * Now add a NULL session info
+        */
+       ldb_set_opaque(ldb, "sessionInfo", NULL);
+       assert_false(dsdb_audit_is_system_session(module));
+
+       /*
+        * Now add a session info with no user sid
+        */
+       sess = talloc_zero(ctx, struct auth_session_info);
+       ldb_set_opaque(ldb, "sessionInfo", sess);
+       assert_false(dsdb_audit_is_system_session(module));
+
+       /*
+        * Now add an empty security token.
+        */
+       token = talloc_zero(ctx, struct security_token);
+       sess->security_token = token;
+       assert_false(dsdb_audit_is_system_session(module));
+
+       /*
+        * Add a single SID, non system sid
+        */
+       string_to_sid(&sids[0], SID0);
+       token->num_sids = 1;
+       token->sids = sids;
+       assert_false(dsdb_audit_is_system_session(module));
+
+       /*
+        * Add the system SID to the second position,
+        * this should be ignored.
+        */
+       token->num_sids = 2;
+       sids[1] = global_sid_System;
+       assert_false(dsdb_audit_is_system_session(module));
+
+       /*
+        * Add a single SID, system sid
+        */
+       token->num_sids = 1;
+       sids[0] = global_sid_System;
+       token->sids = sids;
+       assert_true(dsdb_audit_is_system_session(module));
+
+       /*
+        * Add a non system SID to position 2
+        */
+       sids[0] = global_sid_System;
+       string_to_sid(&sids[1], SID1);
+       token->num_sids = 2;
+       token->sids = sids;
+       assert_true(dsdb_audit_is_system_session(module));
+
+       /*
+        * Now test a null sid in the first position
+        */
+       token->num_sids = 1;
+       token->sids = NULL;
+       sid = dsdb_audit_get_user_sid(module);
+       assert_null(sid);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_get_unique_session_token(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module *module = NULL;
+       struct auth_session_info *sess = NULL;
+       const struct GUID *guid;
+       const char * const GUID_S = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       struct GUID in;
+       char *guid_str;
+       struct GUID_txt_buf guid_buff;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+
+       /*
+        * Test a freshly initialized ldb
+        * should return NULL
+        */
+       guid = dsdb_audit_get_unique_session_token(module);
+       assert_null(guid);
+
+       /*
+        * Now add a NULL session info
+        */
+       ldb_set_opaque(ldb, "sessionInfo", NULL);
+       guid = dsdb_audit_get_unique_session_token(module);
+       assert_null(guid);
+
+       /*
+        * Now add a session info with no session id
+        * Note if the memory has not been zeroed correctly all bets are
+        *      probably off.
+        */
+       sess = talloc_zero(ctx, struct auth_session_info);
+       ldb_set_opaque(ldb, "sessionInfo", sess);
+       guid = dsdb_audit_get_unique_session_token(module);
+       /*
+        * We will get a GUID, but it's contents will be undefined
+        */
+       assert_non_null(guid);
+
+       /*
+        * Now set the session id and confirm that we get it back.
+        */
+       GUID_from_string(GUID_S, &in);
+       sess->unique_session_token = in;
+       guid = dsdb_audit_get_unique_session_token(module);
+       assert_non_null(guid);
+       guid_str = GUID_buf_string(guid, &guid_buff);
+       assert_string_equal(GUID_S, guid_str);
+
+       TALLOC_FREE(ctx);
+
+}
+
+static void test_dsdb_audit_get_actual_unique_session_token(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct auth_session_info *sess = NULL;
+       const struct GUID *guid;
+       const char * const GUID_S = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       struct GUID in;
+       char *guid_str;
+       struct GUID_txt_buf guid_buff;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       /*
+        * Test a freshly initialized ldb
+        * should return NULL
+        */
+       guid = dsdb_audit_get_actual_unique_session_token(ldb);
+       assert_null(guid);
+
+       /*
+        * Now add a NULL session info
+        */
+       ldb_set_opaque(ldb, "networkSessionInfo", NULL);
+       guid = dsdb_audit_get_actual_unique_session_token(ldb);
+       assert_null(guid);
+
+       /*
+        * Now add a session info with no session id
+        * Note if the memory has not been zeroed correctly all bets are
+        *      probably off.
+        */
+       sess = talloc_zero(ctx, struct auth_session_info);
+       ldb_set_opaque(ldb, "networkSessionInfo", sess);
+       guid = dsdb_audit_get_actual_unique_session_token(ldb);
+       /*
+        * We will get a GUID, but it's contents will be undefined
+        */
+       assert_non_null(guid);
+
+       /*
+        * Now set the session id and confirm that we get it back.
+        */
+       GUID_from_string(GUID_S, &in);
+       sess->unique_session_token = in;
+       guid = dsdb_audit_get_actual_unique_session_token(ldb);
+       assert_non_null(guid);
+       guid_str = GUID_buf_string(guid, &guid_buff);
+       assert_string_equal(GUID_S, guid_str);
+
+       TALLOC_FREE(ctx);
+
+}
+
+static void test_dsdb_audit_get_remote_host(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       char *rh = NULL;
+       struct tsocket_address *in = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = talloc_zero(ctx, struct ldb_context);
+
+       /*
+        * Test a freshly initialized ldb
+        * should return "Unknown"
+        */
+       rh = dsdb_audit_get_remote_host(ldb, ctx);
+       assert_string_equal("Unknown", rh);
+       TALLOC_FREE(rh);
+
+       /*
+        * opaque set to null, should return NULL
+        */
+       ldb_set_opaque(ldb, "remoteAddress", NULL);
+       rh = dsdb_audit_get_remote_host(ldb, ctx);
+       assert_string_equal("Unknown", rh);
+       TALLOC_FREE(rh);
+
+       /*
+        * Ensure that the value set is returned
+        */
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 42, &in);
+       ldb_set_opaque(ldb, "remoteAddress", in);
+       rh = dsdb_audit_get_remote_host(ldb, ctx);
+       assert_string_equal("ipv4:127.0.0.1:42", rh);
+       TALLOC_FREE(rh);
+
+       TALLOC_FREE(ctx);
+
+}
+
+static void test_dsdb_audit_get_primary_dn(void **state)
+{
+       struct ldb_request *req = NULL;
+       struct ldb_message *msg = NULL;
+       struct ldb_context *ldb = NULL;
+
+       struct ldb_dn *dn = NULL;
+
+       const char * const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+       const char *s = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       req = talloc_zero(ctx, struct ldb_request);
+       msg = talloc_zero(ctx, struct ldb_message);
+       ldb = talloc_zero(ctx, struct ldb_context);
+       dn = ldb_dn_new(ctx, ldb, DN);
+
+       /*
+        * Try an empty request.
+        */
+       s = dsdb_audit_get_primary_dn(req);
+       assert_null(s);
+
+       /*
+        * Now try an add with a null message.
+        */
+       req->operation = LDB_ADD;
+       req->op.add.message = NULL;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_null(s);
+
+       /*
+        * Now try an mod with a null message.
+        */
+       req->operation = LDB_MODIFY;
+       req->op.mod.message = NULL;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_null(s);
+
+       /*
+        * Now try an add with a missing dn
+        */
+       req->operation = LDB_ADD;
+       req->op.add.message = msg;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_null(s);
+
+       /*
+        * Now try a mod with a messing dn
+        */
+       req->operation = LDB_ADD;
+       req->op.mod.message = msg;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_null(s);
+
+       /*
+        * Add a dn to the message
+        */
+       msg->dn = dn;
+
+       /*
+        * Now try an add with a dn
+        */
+       req->operation = LDB_ADD;
+       req->op.add.message = msg;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_non_null(s);
+       assert_string_equal(DN, s);
+
+       /*
+        * Now try a mod with a dn
+        */
+       req->operation = LDB_MODIFY;
+       req->op.mod.message = msg;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_non_null(s);
+       assert_string_equal(DN, s);
+
+       /*
+        * Try a delete without a dn
+        */
+       req->operation = LDB_DELETE;
+       req->op.del.dn = NULL;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_null(s);
+
+       /*
+        * Try a delete with a dn
+        */
+       req->operation = LDB_DELETE;
+       req->op.del.dn = dn;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_non_null(s);
+       assert_string_equal(DN, s);
+
+       /*
+        * Try a rename without a dn
+        */
+       req->operation = LDB_RENAME;
+       req->op.rename.olddn = NULL;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_null(s);
+
+       /*
+        * Try a rename with a dn
+        */
+       req->operation = LDB_RENAME;
+       req->op.rename.olddn = dn;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_non_null(s);
+       assert_string_equal(DN, s);
+
+       /*
+        * Try an extended operation, i.e. one that does not have a DN
+        * associated with it for logging purposes.
+        */
+       req->operation = LDB_EXTENDED;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_null(s);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_get_message(void **state)
+{
+       struct ldb_request *req = NULL;
+       struct ldb_message *msg = NULL;
+       const struct ldb_message *r = NULL;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       req = talloc_zero(ctx, struct ldb_request);
+       msg = talloc_zero(ctx, struct ldb_message);
+
+       /*
+        * Test an empty message
+        */
+       r = dsdb_audit_get_message(req);
+       assert_null(r);
+
+       /*
+        * Test an add message
+        */
+       req->operation = LDB_ADD;
+       req->op.add.message = msg;
+       r = dsdb_audit_get_message(req);
+       assert_ptr_equal(msg, r);
+
+       /*
+        * Test a modify message
+        */
+       req->operation = LDB_MODIFY;
+       req->op.mod.message = msg;
+       r = dsdb_audit_get_message(req);
+       assert_ptr_equal(msg, r);
+
+       /*
+        * Test a Delete message, i.e. trigger the default case
+        */
+       req->operation = LDB_DELETE;
+       r = dsdb_audit_get_message(req);
+       assert_null(r);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_get_secondary_dn(void **state)
+{
+       struct ldb_request *req = NULL;
+       struct ldb_context *ldb = NULL;
+
+       struct ldb_dn *dn = NULL;
+
+       const char * const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG";
+       const char *s = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       req = talloc_zero(ctx, struct ldb_request);
+       ldb = talloc_zero(ctx, struct ldb_context);
+       dn = ldb_dn_new(ctx, ldb, DN);
+
+       /*
+        * Try an empty request.
+        */
+       s = dsdb_audit_get_secondary_dn(req);
+       assert_null(s);
+
+       /*
+        * Try a rename without a dn
+        */
+       req->operation = LDB_RENAME;
+       req->op.rename.newdn = NULL;
+       s = dsdb_audit_get_secondary_dn(req);
+       assert_null(s);
+
+       /*
+        * Try a rename with a dn
+        */
+       req->operation = LDB_RENAME;
+       req->op.rename.newdn = dn;
+       s = dsdb_audit_get_secondary_dn(req);
+       assert_non_null(s);
+       assert_string_equal(DN, s);
+
+       /*
+        * Try an extended operation, i.e. one that does not have a DN
+        * associated with it for logging purposes.
+        */
+       req->operation = LDB_EXTENDED;
+       s = dsdb_audit_get_primary_dn(req);
+       assert_null(s);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_get_operation_name(void **state)
+{
+       struct ldb_request *req = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       req = talloc_zero(ctx, struct ldb_request);
+
+       req->operation =  LDB_SEARCH;
+       assert_string_equal("Search", dsdb_audit_get_operation_name(req));
+
+       req->operation =  LDB_ADD;
+       assert_string_equal("Add", dsdb_audit_get_operation_name(req));
+
+       req->operation =  LDB_MODIFY;
+       assert_string_equal("Modify", dsdb_audit_get_operation_name(req));
+
+       req->operation =  LDB_DELETE;
+       assert_string_equal("Delete", dsdb_audit_get_operation_name(req));
+
+       req->operation =  LDB_RENAME;
+       assert_string_equal("Rename", dsdb_audit_get_operation_name(req));
+
+       req->operation =  LDB_EXTENDED;
+       assert_string_equal("Extended", dsdb_audit_get_operation_name(req));
+
+       req->operation =  LDB_REQ_REGISTER_CONTROL;
+       assert_string_equal(
+               "Register Control",
+               dsdb_audit_get_operation_name(req));
+
+       req->operation =  LDB_REQ_REGISTER_PARTITION;
+       assert_string_equal(
+               "Register Partition",
+               dsdb_audit_get_operation_name(req));
+
+       /*
+        * Trigger the default case
+        */
+       req->operation =  -1;
+       assert_string_equal("Unknown", dsdb_audit_get_operation_name(req));
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dsdb_audit_get_modification_action(void **state)
+{
+       assert_string_equal(
+               "add",
+               dsdb_audit_get_modification_action(LDB_FLAG_MOD_ADD));
+       assert_string_equal(
+               "delete",
+               dsdb_audit_get_modification_action(LDB_FLAG_MOD_DELETE));
+       assert_string_equal(
+               "replace",
+               dsdb_audit_get_modification_action(LDB_FLAG_MOD_REPLACE));
+       /*
+        * Trigger the default case
+        */
+       assert_string_equal(
+               "unknown",
+               dsdb_audit_get_modification_action(0));
+}
+
+static void test_dsdb_audit_is_password_attribute(void **state)
+{
+       assert_true(dsdb_audit_is_password_attribute("userPassword"));
+       assert_true(dsdb_audit_is_password_attribute("clearTextPassword"));
+       assert_true(dsdb_audit_is_password_attribute("unicodePwd"));
+       assert_true(dsdb_audit_is_password_attribute("dBCSPwd"));
+
+       assert_false(dsdb_audit_is_password_attribute("xserPassword"));
+}
+
+static void test_dsdb_audit_redact_attribute(void **state)
+{
+       assert_true(dsdb_audit_redact_attribute("userPassword"));
+
+       assert_true(dsdb_audit_redact_attribute("pekList"));
+       assert_true(dsdb_audit_redact_attribute("clearTextPassword"));
+       assert_true(dsdb_audit_redact_attribute("initialAuthIncoming"));
+
+       assert_false(dsdb_audit_redact_attribute("supaskrt"));
+}
+
+int main(void) {
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test(test_dsdb_audit_add_ldb_value),
+               cmocka_unit_test(test_dsdb_audit_attributes_json),
+               cmocka_unit_test(test_dsdb_audit_get_remote_address),
+               cmocka_unit_test(test_dsdb_audit_get_ldb_error_string),
+               cmocka_unit_test(test_dsdb_audit_get_user_sid),
+               cmocka_unit_test(test_dsdb_audit_get_actual_sid),
+               cmocka_unit_test(test_dsdb_audit_is_system_session),
+               cmocka_unit_test(test_dsdb_audit_get_unique_session_token),
+               cmocka_unit_test(test_dsdb_audit_get_actual_unique_session_token),
+               cmocka_unit_test(test_dsdb_audit_get_remote_host),
+               cmocka_unit_test(test_dsdb_audit_get_primary_dn),
+               cmocka_unit_test(test_dsdb_audit_get_message),
+               cmocka_unit_test(test_dsdb_audit_get_secondary_dn),
+               cmocka_unit_test(test_dsdb_audit_get_operation_name),
+               cmocka_unit_test(test_dsdb_audit_get_modification_action),
+               cmocka_unit_test(test_dsdb_audit_is_password_attribute),
+               cmocka_unit_test(test_dsdb_audit_redact_attribute),
+       };
+
+       cmocka_set_message_output(CM_OUTPUT_SUBUNIT);
+       return cmocka_run_group_tests(tests, NULL, NULL);
+}
index 9e0ac281cc6b28c370415255d9145abfe11331c5..3e591a0a0d57f0af13b3f799dbc62e909735002e 100644 (file)
@@ -7,9 +7,9 @@ bld.SAMBA_LIBRARY('dsdb-module',
        grouping_library=True)
 
 bld.SAMBA_SUBSYSTEM('DSDB_MODULE_HELPERS',
-       source='util.c acl_util.c schema_util.c netlogon.c',
+       source='util.c acl_util.c schema_util.c netlogon.c audit_util.c',
        autoproto='util_proto.h',
-       deps='ldb ndr samdb-common samba-security'
+       deps='ldb ndr samdb-common samba-security audit_logging'
        )
 
 bld.SAMBA_SUBSYSTEM('DSDB_MODULE_HELPER_RIDALLOC',
@@ -40,6 +40,35 @@ bld.SAMBA_BINARY('test_encrypted_secrets',
             DSDB_MODULE_HELPERS
         ''',
         install=False)
+#
+# These tests require JANSSON, so we only build them if we are doing a selftest
+# build.
+#
+if bld.CONFIG_GET('ENABLE_SELFTEST'):
+    bld.SAMBA_BINARY('test_audit_util',
+            source='tests/test_audit_util.c',
+            deps='''
+                talloc
+                samba-util
+                samdb-common
+                samdb
+                cmocka
+                audit_logging
+                DSDB_MODULE_HELPERS
+            ''',
+            install=False)
+    bld.SAMBA_BINARY('test_audit_log',
+            source='tests/test_audit_log.c',
+            deps='''
+                talloc
+                samba-util
+                samdb-common
+                samdb
+                cmocka
+                audit_logging
+                DSDB_MODULE_HELPERS
+            ''',
+            install=False)
 
 if bld.AD_DC_BUILD_IS_ENABLED():
     bld.PROCESS_SEPARATE_RULE("server")
index 368260afe094b1b4d4ac584fe7500c54a064a910..6c821fb7d7bbab2344ec6281caee75aa69186569 100644 (file)
@@ -425,3 +425,19 @@ bld.SAMBA_MODULE('ldb_encrypted_secrets',
             gnutls
         '''
        )
+
+bld.SAMBA_MODULE('ldb_audit_log',
+       source='audit_log.c',
+       subsystem='ldb',
+       init_function='ldb_audit_log_module_init',
+       module_init_name='ldb_init_module',
+       internal_module=False,
+       deps='''
+            audit_logging
+            talloc
+            samba-util
+            samdb-common
+            DSDB_MODULE_HELPERS
+            samdb
+        '''
+       )
index 069128b2e847153d43a8f6adc662dda37a70a249..a95352fa22fe71e4a42274ea01c05d77e74446d6 100755 (executable)
@@ -685,6 +685,14 @@ if have_heimdal_support:
                            extra_args=['-U"$USERNAME%$PASSWORD"'],
                            environ={'CLIENT_IP': '127.0.0.11',
                                     'SOCKET_WRAPPER_DEFAULT_IFACE': 11})
+    planoldpythontestsuite("ad_dc:local", "samba.tests.audit_log_pass_change",
+                           extra_args=['-U"$USERNAME%$PASSWORD"'],
+                           environ={'CLIENT_IP': '127.0.0.11',
+                                    'SOCKET_WRAPPER_DEFAULT_IFACE': 11})
+    planoldpythontestsuite("ad_dc:local", "samba.tests.audit_log_dsdb",
+                           extra_args=['-U"$USERNAME%$PASSWORD"'],
+                           environ={'CLIENT_IP': '127.0.0.11',
+                                    'SOCKET_WRAPPER_DEFAULT_IFACE': 11})
 
 planoldpythontestsuite("fl2008r2dc:local",
                        "samba.tests.getdcname",
@@ -1075,3 +1083,7 @@ plantestsuite("samba4.dsdb.samdb.ldb_modules.encrypted_secrets", "none",
                   [os.path.join(bindir(), "test_encrypted_secrets")])
 plantestsuite("lib.audit_logging.audit_logging", "none",
                   [os.path.join(bindir(), "audit_logging_test")])
+plantestsuite("samba4.dsdb.samdb.ldb_modules.audit_util", "none",
+                  [os.path.join(bindir(), "test_audit_util")])
+plantestsuite("samba4.dsdb.samdb.ldb_modules.audit_log", "none",
+                  [os.path.join(bindir(), "test_audit_log")])