dsdb: Audit group membership changes
authorGary Lockyer <gary@catalyst.net.nz>
Mon, 16 Apr 2018 02:03:14 +0000 (14:03 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Sat, 9 Jun 2018 13:02:11 +0000 (15:02 +0200)
Log details of Group membership changes and User Primary Group changes.
Changes are logged in human readable and if samba has been built with
JANSSON support in JSON format.

Replicated updates are not logged.

Signed-off-by: Gary Lockyer <gary@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/tests/group_audit.py [new file with mode: 0644]
selftest/target/Samba4.pm
source4/dsdb/samdb/ldb_modules/group_audit.c [new file with mode: 0644]
source4/dsdb/samdb/ldb_modules/samba_dsdb.c
source4/dsdb/samdb/ldb_modules/tests/test_group_audit.c [new file with mode: 0644]
source4/dsdb/samdb/ldb_modules/tests/test_group_audit.valgrind [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/group_audit.py b/python/samba/tests/group_audit.py
new file mode 100644 (file)
index 0000000..53a8bf6
--- /dev/null
@@ -0,0 +1,355 @@
+# 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_GROUP_LOG, DSDB_GROUP_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
+import ldb
+from ldb import FLAG_MOD_REPLACE
+
+USER_NAME = "grpadttstuser01"
+USER_PASS = samba.generate_random_password(32, 32)
+
+SECOND_USER_NAME = "grpadttstuser02"
+SECOND_USER_PASS = samba.generate_random_password(32, 32)
+
+GROUP_NAME_01 = "group-audit-01"
+GROUP_NAME_02 = "group-audit-02"
+
+
+class GroupAuditTests(AuditLogTestBase):
+
+    def setUp(self):
+        self.message_type = MSG_GROUP_LOG
+        self.event_type   = DSDB_GROUP_EVENT_NAME
+        super(GroupAuditTests, 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
+        self.ldb.add({
+            "dn": "cn=" + USER_NAME + ",cn=users," + self.base_dn,
+            "objectclass": "user",
+            "sAMAccountName": USER_NAME,
+            "userPassword": USER_PASS
+        })
+        self.ldb.newgroup(GROUP_NAME_01)
+        self.ldb.newgroup(GROUP_NAME_02)
+
+    def tearDown(self):
+        super(GroupAuditTests, self).tearDown()
+        delete_force(self.ldb, "cn=" + USER_NAME + ",cn=users," + self.base_dn)
+        self.ldb.deletegroup(GROUP_NAME_01)
+        self.ldb.deletegroup(GROUP_NAME_02)
+
+    def test_add_and_remove_users_from_group(self):
+
+        #
+        # Wait for the primary group change for the created user.
+        #
+        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]["groupChange"]
+
+        self.assertEqual("PrimaryGroup", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=domain users,cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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")
+
+        #
+        # Add the user to a group
+        #
+        self.discardMessages()
+
+        self.ldb.add_remove_group_members(GROUP_NAME_01, [USER_NAME])
+        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]["groupChange"]
+
+        self.assertEqual("Added", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=" + GROUP_NAME_01 + ",cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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")
+
+        #
+        # Add the user to another group
+        #
+        self.discardMessages()
+        self.ldb.add_remove_group_members(GROUP_NAME_02, [USER_NAME])
+
+        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]["groupChange"]
+
+        self.assertEqual("Added", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=" + GROUP_NAME_02 + ",cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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")
+
+        #
+        # Remove the user from a group
+        #
+        self.discardMessages()
+        self.ldb.add_remove_group_members(
+            GROUP_NAME_01,
+            [USER_NAME],
+            add_members_operation=False)
+        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]["groupChange"]
+
+        self.assertEqual("Removed", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=" + GROUP_NAME_01 + ",cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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")
+
+        #
+        # Re-add the user to a group
+        #
+        self.discardMessages()
+        self.ldb.add_remove_group_members(GROUP_NAME_01, [USER_NAME])
+
+        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]["groupChange"]
+
+        self.assertEqual("Added", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=" + GROUP_NAME_01 + ",cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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_change_primary_group(self):
+
+        #
+        # Wait for the primary group change for the created user.
+        #
+        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]["groupChange"]
+
+        self.assertEqual("PrimaryGroup", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=domain users,cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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")
+
+        #
+        # Add the user to a group, the user needs to be a member of a group
+        # before there primary group can be set to that group.
+        #
+        self.discardMessages()
+
+        self.ldb.add_remove_group_members(GROUP_NAME_01, [USER_NAME])
+        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]["groupChange"]
+
+        self.assertEqual("Added", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=" + GROUP_NAME_01 + ",cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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")
+
+        #
+        # Change the primary group of a user
+        #
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=" + GROUP_NAME_01 + ",cn=users," + self.base_dn
+        # get the primaryGroupToken of the group
+        res = self.ldb.search(base=group_dn, attrs=["primaryGroupToken"],
+                              scope=ldb.SCOPE_BASE)
+        group_id = res[0]["primaryGroupToken"]
+
+        # set primaryGroupID attribute of the user to that group
+        m = ldb.Message()
+        m.dn = ldb.Dn(self.ldb, user_dn)
+        m["primaryGroupID"] = ldb.MessageElement(
+            group_id,
+            FLAG_MOD_REPLACE,
+            "primaryGroupID")
+        self.discardMessages()
+        self.ldb.modify(m)
+
+        #
+        # Wait for the primary group change.
+        # Will see the user removed from the new group
+        #          the user added to their old primary group
+        #          and a new primary group event.
+        #
+        messages = self.waitForMessages(3)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(3,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        audit = messages[0]["groupChange"]
+        self.assertEqual("Removed", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=" + GROUP_NAME_01 + ",cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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")
+
+        audit = messages[1]["groupChange"]
+
+        self.assertEqual("Added", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=domain users,cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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")
+
+        audit = messages[2]["groupChange"]
+
+        self.assertEqual("PrimaryGroup", audit["action"])
+        user_dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        group_dn = "cn=" + GROUP_NAME_01 + ",cn=users," + self.base_dn
+        self.assertTrue(user_dn.lower(), audit["user"].lower())
+        self.assertTrue(group_dn.lower(), audit["group"].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")
index 3df226f80d206f9c6fdc06dfb9585679c84e5091..7abc16e1a7af5fd7a3428f5bb96d59fabdf19c5b 100755 (executable)
@@ -1526,6 +1526,7 @@ sub provision_ad_dc_ntvfs($$)
         auth event notification = true
        dsdb event notification = true
        dsdb password event notification = true
+       dsdb group change notification = true
        server schannel = auto
        ";
        my $ret = $self->provision($prefix,
@@ -1900,6 +1901,7 @@ sub provision_ad_dc($$$$$$)
         auth event notification = true
        dsdb event notification = true
        dsdb password event notification = true
+       dsdb group change notification = true
         $smbconf_args
 ";
 
diff --git a/source4/dsdb/samdb/ldb_modules/group_audit.c b/source4/dsdb/samdb/ldb_modules/group_audit.c
new file mode 100644 (file)
index 0000000..dc58677
--- /dev/null
@@ -0,0 +1,1362 @@
+/*
+   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 group memberships
+ *
+ */
+
+#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 AUDIT_JSON_TYPE "groupChange"
+#define AUDIT_HR_TAG "Group Change"
+#define AUDIT_MAJOR 1
+#define AUDIT_MINOR 0
+#define GROUP_LOG_LVL 5
+
+static const char * const member_attr[] = {"member", NULL};
+static const char * const primary_group_attr[] = {
+       "primaryGroupID",
+       "objectSID",
+       NULL};
+
+struct audit_context {
+       bool send_events;
+       struct imessaging_context *msg_ctx;
+};
+
+struct audit_callback_context {
+       struct ldb_request *request;
+       struct ldb_module *module;
+       struct ldb_message_element *members;
+       uint32_t primary_group;
+       void (*log_changes)(
+               struct audit_callback_context *acc,
+               const int status);
+};
+
+/*
+ * @brief get the transaction id.
+ *
+ * Get the id of the transaction that the current request is contained in.
+ *
+ * @param req the request.
+ *
+ * @return the transaction id GUID, or NULL if it is not there.
+ */
+static struct GUID *get_transaction_id(
+       const struct ldb_request *request)
+{
+       struct ldb_control *control;
+       struct dsdb_control_transaction_identifier *transaction_id;
+
+       control = ldb_request_get_control(
+               discard_const(request),
+               DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID);
+       if (control == NULL) {
+               return NULL;
+       }
+       transaction_id = talloc_get_type(
+               control->data,
+               struct dsdb_control_transaction_identifier);
+       if (transaction_id == NULL) {
+               return NULL;
+       }
+       return &transaction_id->transaction_guid;
+}
+
+#ifdef HAVE_JANSSON
+/*
+ * @brief generate a JSON log entry for a group change.
+ *
+ * Generate a JSON object containing details of a users group change.
+ *
+ * @param module the ldb module
+ * @param request the ldb_request
+ * @param action the change action being performed
+ * @param user the user name
+ * @param group the group name
+ * @param status the ldb status code for the ldb operation.
+ *
+ * @return A json object containing the details.
+ */
+static struct json_object audit_group_json(
+       const struct ldb_module *module,
+       const struct ldb_request *request,
+       const char *action,
+       const char *user,
+       const char *group,
+       const int status)
+{
+       struct ldb_context *ldb = NULL;
+       const struct dom_sid *sid = NULL;
+       struct json_object wrapper;
+       struct json_object audit;
+       const struct tsocket_address *remote = NULL;
+       const struct GUID *unique_session_token = NULL;
+       struct GUID *transaction_id = NULL;
+
+       ldb = ldb_module_get_ctx(discard_const(module));
+
+       remote = dsdb_audit_get_remote_address(ldb);
+       sid = dsdb_audit_get_user_sid(module);
+       unique_session_token = dsdb_audit_get_unique_session_token(module);
+       transaction_id = get_transaction_id(request);
+
+       audit = json_new_object();
+       json_add_version(&audit, AUDIT_MAJOR, AUDIT_MINOR);
+       json_add_int(&audit, "statusCode", status);
+       json_add_string(&audit, "status", ldb_strerror(status));
+       json_add_string(&audit, "action", action);
+       json_add_address(&audit, "remoteAddress", remote);
+       json_add_sid(&audit, "userSid", sid);
+       json_add_string(&audit, "group", group);
+       json_add_guid(&audit, "transactionId", transaction_id);
+       json_add_guid(&audit, "sessionId", unique_session_token);
+       json_add_string(&audit, "user", user);
+
+       wrapper = json_new_object();
+       json_add_timestamp(&wrapper);
+       json_add_string(&wrapper, "type", AUDIT_JSON_TYPE);
+       json_add_object(&wrapper, AUDIT_JSON_TYPE, &audit);
+
+       return wrapper;
+}
+#endif
+
+/*
+ * @brief generate a human readable log entry for a group change.
+ *
+ * Generate a human readable log entry containing details of a users group
+ * change.
+ *
+ * @param ctx the talloc context owning the returned log entry
+ * @param module the ldb module
+ * @param request the ldb_request
+ * @param action the change action being performed
+ * @param user the user name
+ * @param group the group name
+ * @param status the ldb status code for the ldb operation.
+ *
+ * @return A human readable log line.
+ */
+static char *audit_group_human_readable(
+       TALLOC_CTX *mem_ctx,
+       const struct ldb_module *module,
+       const struct ldb_request *request,
+       const char *action,
+       const char *user,
+       const char *group,
+       const int status)
+{
+       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;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = ldb_module_get_ctx(discard_const(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);
+
+       log_entry = talloc_asprintf(
+               mem_ctx,
+               "[%s] at [%s] status [%s] "
+               "Remote host [%s] SID [%s] Group [%s] User [%s]",
+               action,
+               timestamp,
+               ldb_strerror(status),
+               remote_host,
+               user_sid,
+               group,
+               user);
+       TALLOC_FREE(ctx);
+       return log_entry;
+}
+
+/*
+ * @brief generate an array of parsed_dns, deferring the actual parsing.
+ *
+ * Get an array of 'struct parsed_dns' without the parsing.
+ * The parsed_dns are parsed only when needed to avoid the expense of parsing.
+ *
+ * This procedure assumes that the dn's are sorted in GUID order and contains
+ * no duplicates.  This should be valid as the module sits below repl_meta_data
+ * which ensures this.
+ *
+ * @param mem_ctx The memory context that will own the generated array
+ * @param el The message element used to generate the array.
+ *
+ * @return an array of struct parsed_dns, or NULL in the event of an error
+ */
+static struct parsed_dn *get_parsed_dns(
+       TALLOC_CTX *mem_ctx,
+       struct ldb_message_element *el)
+{
+       struct parsed_dn *pdn = NULL;
+
+       int i;
+
+       if (el == NULL || el->num_values == 0) {
+               return NULL;
+       }
+
+       pdn = talloc_zero_array(mem_ctx, struct parsed_dn, el->num_values);
+       if (pdn == NULL) {
+               DBG_ERR("Out of memory\n");
+               return NULL;
+       }
+
+       for (i = 0; i < el->num_values; i++) {
+               pdn[i].v = &el->values[i];
+       }
+       return pdn;
+
+}
+
+enum dn_compare_result {
+       LESS_THAN,
+       BINARY_EQUAL,
+       EQUAL,
+       GREATER_THAN
+};
+/*
+ * @brief compare parsed_dns
+ *
+ * Compare two parsed_dn structures, parsing the entries if necessary.
+ * To avoid the overhead of parsing the DN's this function does a binary
+ * compare first. Only parsing the DN's they are not equal at a binary level.
+ *
+ * @param ctx talloc context that will own the parsed dsdb_dn
+ * @param ldb ldb_context
+ * @param old_val The old value
+ * @param new_val The old value
+ *
+ * @return BINARY_EQUAL values are equal at a binary level
+ *         EQUAL        DN's are equal but the meta data is different
+ *         LESS_THAN    old value < new value
+ *         GREATER_THAN old value > new value
+ *
+ */
+static enum dn_compare_result dn_compare(
+       TALLOC_CTX *mem_ctx,
+       struct ldb_context *ldb,
+       struct parsed_dn *old_val,
+       struct parsed_dn *new_val) {
+
+       int res = 0;
+
+       /*
+        * Do a binary compare first to avoid unnecessary parsing
+        */
+       if (data_blob_cmp(new_val->v, old_val->v) == 0) {
+               /*
+                * Values are equal at a binary level so no need
+                * for further processing
+                */
+               return BINARY_EQUAL;
+       }
+       /*
+        * Values not equal at the binary level, so lets
+        * do a GUID ordering compare. To do this we will need to ensure
+        * that the dn's have been parsed.
+        */
+       if (old_val->dsdb_dn == NULL) {
+               really_parse_trusted_dn(
+                       mem_ctx,
+                       ldb,
+                       old_val,
+                       LDB_SYNTAX_DN);
+       }
+       if (new_val->dsdb_dn == NULL) {
+               really_parse_trusted_dn(
+                       mem_ctx,
+                       ldb,
+                       new_val,
+                       LDB_SYNTAX_DN);
+       }
+
+       res = ndr_guid_compare(&new_val->guid, &old_val->guid);
+       if (res < 0) {
+               return LESS_THAN;
+       } else if (res == 0) {
+               return EQUAL;
+       } else {
+               return GREATER_THAN;
+       }
+}
+
+/*
+ * @brief Get the DN of a users primary group as a printable string.
+ *
+ * Get the DN of a users primary group as a printable string.
+ *
+ * @param mem_ctx Talloc context the the returned string will be allocated on.
+ * @param module The ldb module
+ * @param account_sid The SID for the uses account.
+ * @param primary_group_rid The RID for the users primary group.
+ *
+ * @return a formatted DN, or null if there is an error.
+ */
+static const char *get_primary_group_dn(
+       TALLOC_CTX *mem_ctx,
+       struct ldb_module *module,
+       struct dom_sid *account_sid,
+       uint32_t primary_group_rid)
+{
+       NTSTATUS status;
+
+       struct ldb_context *ldb = NULL;
+       struct dom_sid *domain_sid = NULL;
+       struct dom_sid *primary_group_sid = NULL;
+       char *sid = NULL;
+       struct ldb_dn *dn = NULL;
+       struct ldb_message *msg = NULL;
+       int rc;
+
+       ldb = ldb_module_get_ctx(module);
+
+       status = dom_sid_split_rid(mem_ctx, account_sid, &domain_sid, NULL);
+       if (!NT_STATUS_IS_OK(status)) {
+               return NULL;
+       }
+
+       primary_group_sid = dom_sid_add_rid(
+               mem_ctx,
+               domain_sid,
+               primary_group_rid);
+       if (!primary_group_sid) {
+               return NULL;
+       }
+
+       sid = dom_sid_string(mem_ctx, primary_group_sid);
+       if (sid == NULL) {
+               return NULL;
+       }
+
+       dn = ldb_dn_new_fmt(mem_ctx, ldb, "<SID=%s>", sid);
+       if(dn == NULL) {
+               return sid;
+       }
+       rc = dsdb_search_one(
+               ldb,
+               mem_ctx,
+               &msg,
+               dn,
+               LDB_SCOPE_BASE,
+               NULL,
+               0,
+               NULL);
+       if (rc != LDB_SUCCESS) {
+               return NULL;
+       }
+
+       return ldb_dn_get_linearized(msg->dn);
+}
+
+/*
+ * @brief Log details of a change to a users primary group.
+ *
+ * Log details of a change to a users primary group.
+ *
+ * @param module The ldb module.
+ * @param request The request deing logged.
+ * @param action Description of the action being performed.
+ * @param group The linearized for of the group DN
+ * @param status the LDB status code for the processing of the request.
+ *
+ */
+static void log_primary_group_change(
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const char *action,
+       const char *group,
+       const int  status)
+{
+       const char *user = NULL;
+
+       struct audit_context *ac =
+               talloc_get_type(
+                       ldb_module_get_private(module),
+                       struct audit_context);
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       user = dsdb_audit_get_primary_dn(request);
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL)) {
+               char *message = NULL;
+               message = audit_group_human_readable(
+                       ctx,
+                       module,
+                       request,
+                       action,
+                       user,
+                       group,
+                       status);
+               audit_log_human_text(
+                       AUDIT_HR_TAG,
+                       message,
+                       DBGC_DSDB_GROUP_AUDIT,
+                       GROUP_LOG_LVL);
+               TALLOC_FREE(message);
+       }
+
+#ifdef HAVE_JANSSON
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) ||
+               (ac->msg_ctx && ac->send_events)) {
+
+               struct json_object json;
+               json = audit_group_json(
+                       module,
+                       request,
+                       action,
+                       user,
+                       group,
+                       status);
+               audit_log_json(
+                       AUDIT_JSON_TYPE,
+                       &json,
+                       DBGC_DSDB_GROUP_AUDIT_JSON,
+                       GROUP_LOG_LVL);
+               if (ac->send_events) {
+                       audit_message_send(
+                               ac->msg_ctx,
+                               DSDB_GROUP_EVENT_NAME,
+                               MSG_GROUP_LOG,
+                               &json);
+               }
+               json_free(&json);
+       }
+#endif
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * @brief Log details of a single change to a users group membership.
+ *
+ * Log details of a change to a users group membership, except for changes
+ * to their primary group which is handled by log_primary_group_change.
+ *
+ * @param module The ldb module.
+ * @param request The request being logged.
+ * @param action Description of the action being performed.
+ * @param user The linearized form of the users DN
+ * @param status the LDB status code for the processing of the request.
+ *
+ */
+static void log_membership_change(
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       const char *action,
+       const char *user,
+       const int  status)
+{
+       const char *group = NULL;
+       struct audit_context *ac =
+               talloc_get_type(
+                       ldb_module_get_private(module),
+                       struct audit_context);
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+       group = dsdb_audit_get_primary_dn(request);
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL)) {
+               char *message = NULL;
+               message = audit_group_human_readable(
+                       ctx,
+                       module,
+                       request,
+                       action,
+                       user,
+                       group,
+                       status);
+               audit_log_human_text(
+                       AUDIT_HR_TAG,
+                       message,
+                       DBGC_DSDB_GROUP_AUDIT,
+                       GROUP_LOG_LVL);
+               TALLOC_FREE(message);
+       }
+
+#ifdef HAVE_JANSSON
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) ||
+               (ac->msg_ctx && ac->send_events)) {
+               struct json_object json;
+               json = audit_group_json(
+                       module,
+                       request,
+                       action,
+                       user,
+                       group,
+                       status);
+               audit_log_json(
+                       AUDIT_JSON_TYPE,
+                       &json,
+                       DBGC_DSDB_GROUP_AUDIT_JSON,
+                       GROUP_LOG_LVL);
+               if (ac->send_events) {
+                       audit_message_send(
+                               ac->msg_ctx,
+                               DSDB_GROUP_EVENT_NAME,
+                               MSG_GROUP_LOG,
+                               &json);
+               }
+               json_free(&json);
+       }
+#endif
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * @brief Log all the changes to a users group membership.
+ *
+ * Log details of a change to a users group memberships, except for changes
+ * to their primary group which is handled by log_primary_group_change.
+ *
+ * @param module The ldb module.
+ * @param request The request being logged.
+ * @param action Description of the action being performed.
+ * @param user The linearized form of the users DN
+ * @param status the LDB status code for the processing of the request.
+ *
+ */
+static void log_membership_changes(
+       struct ldb_module *module,
+       const struct ldb_request *request,
+       struct ldb_message_element *el,
+       struct ldb_message_element *old_el,
+       int status)
+{
+       unsigned int i, old_i, new_i;
+       unsigned int old_num_values;
+       unsigned int max_num_values;
+       unsigned int new_num_values;
+       struct parsed_dn *old_val = NULL;
+       struct parsed_dn *new_val = NULL;
+       struct parsed_dn *new_values = NULL;
+       struct parsed_dn *old_values = NULL;
+       struct ldb_context *ldb = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       old_num_values = old_el ? old_el->num_values : 0;
+       new_num_values = el ? el->num_values : 0;
+       max_num_values = old_num_values + new_num_values;
+
+       if (max_num_values == 0) {
+               /*
+                * There is nothing to do!
+                */
+               TALLOC_FREE(ctx);
+               return;
+       }
+
+       old_values = get_parsed_dns(ctx, old_el);
+       new_values = get_parsed_dns(ctx, el);
+       ldb = ldb_module_get_ctx(module);
+
+       old_i = 0;
+       new_i = 0;
+       for (i = 0; i < max_num_values; i++) {
+               enum dn_compare_result cmp;
+               if (old_i < old_num_values && new_i < new_num_values) {
+                       /*
+                        * Both list have values, so compare the values
+                        */
+                       old_val = &old_values[old_i];
+                       new_val = &new_values[new_i];
+                       cmp = dn_compare(ctx, ldb, old_val, new_val);
+               } else if (old_i < old_num_values) {
+                       /*
+                        * the new list is empty, read the old list
+                        */
+                       old_val = &old_values[old_i];
+                       new_val = NULL;
+                       cmp = LESS_THAN;
+               } else if (new_i < new_num_values) {
+                       /*
+                        * the old list is empty, read new list
+                        */
+                       old_val = NULL;
+                       new_val = &new_values[new_i];
+                       cmp = GREATER_THAN;
+               } else {
+                       break;
+               }
+
+               if (cmp == LESS_THAN) {
+                       /*
+                        * Have an entry in the original record that is not in
+                        * the new record. So it's been deleted
+                        */
+                       const char *user = NULL;
+                       if (old_val->dsdb_dn == NULL) {
+                               really_parse_trusted_dn(
+                                       ctx,
+                                       ldb,
+                                       old_val,
+                                       LDB_SYNTAX_DN);
+                       }
+                       user = ldb_dn_get_linearized(old_val->dsdb_dn->dn);
+                       log_membership_change(
+                               module,
+                               request,
+                               "Removed",
+                               user,
+                               status);
+                       old_i++;
+               } else if (cmp == BINARY_EQUAL) {
+                       /*
+                        * DN's unchanged at binary level so nothing to do.
+                        */
+                       old_i++;
+                       new_i++;
+               } else if (cmp == EQUAL) {
+                       /*
+                        * DN is unchanged now need to check the flags to
+                        * determine if a record has been deleted or undeleted
+                        */
+                       uint32_t old_flags;
+                       uint32_t new_flags;
+                       if (old_val->dsdb_dn == NULL) {
+                               really_parse_trusted_dn(
+                                       ctx,
+                                       ldb,
+                                       old_val,
+                                       LDB_SYNTAX_DN);
+                       }
+                       if (new_val->dsdb_dn == NULL) {
+                               really_parse_trusted_dn(
+                                       ctx,
+                                       ldb,
+                                       new_val,
+                                       LDB_SYNTAX_DN);
+                       }
+
+                       dsdb_get_extended_dn_uint32(
+                               old_val->dsdb_dn->dn,
+                               &old_flags,
+                               "RMD_FLAGS");
+                       dsdb_get_extended_dn_uint32(
+                               new_val->dsdb_dn->dn,
+                               &new_flags,
+                               "RMD_FLAGS");
+                       if (new_flags == old_flags) {
+                               /*
+                                * No changes to the Repl meta data so can
+                                * no need to log the change
+                                */
+                               old_i++;
+                               new_i++;
+                               continue;
+                       }
+                       if (new_flags & DSDB_RMD_FLAG_DELETED) {
+                               /*
+                                * DN has been deleted.
+                                */
+                               const char *user = NULL;
+                               user = ldb_dn_get_linearized(
+                                       old_val->dsdb_dn->dn);
+                               log_membership_change(
+                                       module,
+                                       request,
+                                       "Removed",
+                                       user,
+                                       status);
+                       } else {
+                               /*
+                                * DN has been re-added
+                                */
+                               const char *user = NULL;
+                               user = ldb_dn_get_linearized(
+                                       new_val->dsdb_dn->dn);
+                               log_membership_change(
+                                       module,
+                                       request,
+                                       "Added",
+                                       user,
+                                       status);
+                       }
+                       old_i++;
+                       new_i++;
+               } else {
+                       /*
+                        * Member in the updated record that's not in the
+                        * original, so it must have been added.
+                        */
+                       const char *user = NULL;
+                       if ( new_val->dsdb_dn == NULL) {
+                               really_parse_trusted_dn(
+                                       ctx,
+                                       ldb,
+                                       new_val,
+                                       LDB_SYNTAX_DN);
+                       }
+                       user = ldb_dn_get_linearized(new_val->dsdb_dn->dn);
+                       log_membership_change(
+                               module,
+                               request,
+                               "Added",
+                               user,
+                               status);
+                       new_i++;
+               }
+       }
+
+       TALLOC_FREE(ctx);
+}
+
+
+/*
+ * @brief Log the details of a primary group change.
+ *
+ * Retrieve the users primary groupo after the operation has completed
+ * and call log_primary_group_change to log the actual changes.
+ *
+ * @param acc details of the primary group before the operation.
+ * @param status The status code returned by the operation.
+ *
+ * @return an LDB status code.
+ */
+static void log_user_primary_group_change(
+       struct audit_callback_context *acc,
+       const int status)
+{
+       TALLOC_CTX *ctx = talloc_new(NULL);
+       uint32_t new_rid;
+       struct dom_sid *account_sid = NULL;
+       int ret;
+       const struct ldb_message *msg = dsdb_audit_get_message(acc->request);
+       if (status == LDB_SUCCESS && msg != NULL) {
+               struct ldb_result *res = NULL;
+               ret = dsdb_module_search_dn(
+                       acc->module,
+                       ctx,
+                       &res,
+                       msg->dn,
+                       primary_group_attr,
+                       DSDB_FLAG_NEXT_MODULE |
+                       DSDB_SEARCH_REVEAL_INTERNALS |
+                       DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT,
+                       NULL);
+               if (ret == LDB_SUCCESS) {
+                       new_rid = ldb_msg_find_attr_as_uint(
+                               msg,
+                               "primaryGroupID",
+                               ~0);
+                       account_sid = samdb_result_dom_sid(
+                               ctx,
+                               res->msgs[0],
+                               "objectSid");
+               }
+       }
+       /*
+        * If we don't have a new value then the user has been deleted
+        * which we currently do not log.
+        * Otherwise only log if the primary group has actually changed.
+        */
+       if (account_sid != NULL &&
+           new_rid != ~0 &&
+           acc->primary_group != new_rid) {
+               const char* group = get_primary_group_dn(
+                       ctx,
+                       acc->module,
+                       account_sid,
+                       new_rid);
+               log_primary_group_change(
+                       acc->module,
+                       acc->request,
+                       "PrimaryGroup",
+                       group,
+                       status);
+       }
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * @brief log the changes to users group membership.
+ *
+ * Retrieve the users group memberships after the operation has completed
+ * and call log_membership_changes to log the actual changes.
+ *
+ * @param acc details of the group memberships before the operation.
+ * @param status The status code returned by the operation.
+ *
+ * @return an LDB status code.
+ */
+static void log_group_membership_changes(
+       struct audit_callback_context *acc,
+       const int status)
+{
+       TALLOC_CTX *ctx = talloc_new(NULL);
+       struct ldb_message_element *new_val = NULL;
+       int ret;
+       const struct ldb_message *msg = dsdb_audit_get_message(acc->request);
+       if (status == LDB_SUCCESS && msg != NULL) {
+               struct ldb_result *res = NULL;
+               ret = dsdb_module_search_dn(
+                       acc->module,
+                       ctx,
+                       &res,
+                       msg->dn,
+                       member_attr,
+                       DSDB_FLAG_NEXT_MODULE |
+                       DSDB_SEARCH_REVEAL_INTERNALS |
+                       DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT,
+                       NULL);
+               if (ret == LDB_SUCCESS) {
+                       new_val = ldb_msg_find_element(res->msgs[0], "member");
+               }
+       }
+       log_membership_changes(
+               acc->module,
+               acc->request,
+               new_val,
+               acc->members,
+               status);
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * @brief call back function to log changes to the group memberships.
+ *
+ * Call back function to log changes to the uses broup memberships.
+ *
+ * @param req the ldb request.
+ * @param ares the ldb result
+ *
+ * @return am LDB status code.
+ */
+static int group_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 on DONE now we have a result code
+                */
+               ac->log_changes(ac, ares->error);
+               return ldb_module_done(
+                       ac->request,
+                       ares->controls,
+                       ares->response,
+                       ares->error);
+               break;
+
+       default:
+               /* Can't happen */
+               return LDB_ERR_OPERATIONS_ERROR;
+       }
+}
+
+/*
+ * @brief Does this request change the primary group.
+ *
+ * Does the request change the primary group, i.e. does it contain the
+ * primaryGroupID attribute.
+ *
+ * @param req the request to examine.
+ *
+ * @return True if the request modifies the primary group.
+ */
+static bool has_primary_group_id(struct ldb_request *req)
+{
+       struct ldb_message_element *el = NULL;
+       const struct ldb_message *msg = NULL;
+
+       msg = dsdb_audit_get_message(req);
+       el = ldb_msg_find_element(msg, "primaryGroupID");
+
+       return (el != NULL);
+}
+
+/*
+ * @brief Does this request change group membership.
+ *
+ * Does the request change the ses group memberships, i.e. does it contain the
+ * member attribute.
+ *
+ * @param req the request to examine.
+ *
+ * @return True if the request modifies the users group memberships.
+ */
+static bool has_group_membership_changes(struct ldb_request *req)
+{
+       struct ldb_message_element *el = NULL;
+       const struct ldb_message *msg = NULL;
+
+       msg = dsdb_audit_get_message(req);
+       el = ldb_msg_find_element(msg, "member");
+
+       return (el != NULL);
+}
+
+
+
+/*
+ * @brief Install the callback function to log an add request.
+ *
+ * Install the callback function to log an add request changing the users
+ * group memberships. As we want to log the returned status code, we need to
+ * register a callback function that will be called once the operation has
+ * completed.
+ *
+ * This function reads the current user record so that we can log the before
+ * and after state.
+ *
+ * @param module The ldb module.
+ * @param req The modify request.
+ *
+ * @return and LDB status code.
+ */
+static int set_group_membership_add_callback(
+       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;
+       /*
+        * Adding group memberships so will need to log the changes.
+        */
+       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;
+       context->log_changes = log_group_membership_changes;
+       /*
+        * 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,
+               group_audit_callback,
+               req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       return ldb_next_request(module, new_req);
+}
+
+
+/*
+ * @brief Install the callback function to log a modify request.
+ *
+ * Install the callback function to log a modify request changing the primary
+ * group . As we want to log the returned status code, we need to register a
+ * callback function that will be called once the operation has completed.
+ *
+ * This function reads the current user record so that we can log the before
+ * and after state.
+ *
+ * @param module The ldb module.
+ * @param req The modify request.
+ *
+ * @return and LDB status code.
+ */
+static int set_primary_group_modify_callback(
+       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;
+       const struct ldb_message *msg = NULL;
+       struct ldb_result *res = NULL;
+       int ret;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = ldb_module_get_ctx(module);
+
+       context = talloc_zero(req, struct audit_callback_context);
+       if (context == NULL) {
+               ret = ldb_oom(ldb);
+               goto exit;
+       }
+       context->request = req;
+       context->module = module;
+       context->log_changes = log_user_primary_group_change;
+
+       msg = dsdb_audit_get_message(req);
+       ret = dsdb_module_search_dn(
+               module,
+               ctx,
+               &res,
+               msg->dn,
+               primary_group_attr,
+               DSDB_FLAG_NEXT_MODULE |
+               DSDB_SEARCH_REVEAL_INTERNALS |
+               DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT,
+               NULL);
+       if (ret == LDB_SUCCESS) {
+               uint32_t pg;
+               pg = ldb_msg_find_attr_as_uint(
+                       res->msgs[0],
+                       "primaryGroupID",
+                       ~0);
+               context->primary_group = pg;
+       }
+       /*
+        * 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.add.message,
+               req->controls,
+               context,
+               group_audit_callback,
+               req);
+       if (ret != LDB_SUCCESS) {
+               goto exit;
+       }
+       ret = ldb_next_request(module, new_req);
+exit:
+       TALLOC_FREE(ctx);
+       return ret;
+}
+
+/*
+ * @brief Install the callback function to log an add request.
+ *
+ * Install the callback function to log an add request changing the primary
+ * group . As we want to log the returned status code, we need to register a
+ * callback function that will be called once the operation has completed.
+ *
+ * This function reads the current user record so that we can log the before
+ * and after state.
+ *
+ * @param module The ldb module.
+ * @param req The modify request.
+ *
+ * @return and LDB status code.
+ */
+static int set_primary_group_add_callback(
+       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;
+       /*
+        * Adding a user with a primary group.
+        */
+       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;
+       context->log_changes = log_user_primary_group_change;
+       /*
+        * 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,
+               group_audit_callback,
+               req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       return ldb_next_request(module, new_req);
+}
+
+/*
+ * @brief Module handler for add operations.
+ *
+ * Inspect the current add request, and if needed log any group membership
+ * changes.
+ *
+ * @param module The ldb module.
+ * @param req The modify request.
+ *
+ * @return and LDB status code.
+ */
+static int group_add(
+       struct ldb_module *module,
+       struct ldb_request *req)
+{
+
+       struct audit_context *ac =
+               talloc_get_type(
+                       ldb_module_get_private(module),
+                       struct audit_context);
+       /*
+        * Currently we don't log replicated group changes
+        */
+       if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) {
+               return ldb_next_request(module, req);
+       }
+
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL) ||
+               CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) ||
+               (ac->msg_ctx && ac->send_events)) {
+               /*
+                * Avoid the overheads of logging unless it has been
+                * enabled
+                */
+               if (has_group_membership_changes(req)) {
+                       return set_group_membership_add_callback(module, req);
+               }
+               if (has_primary_group_id(req)) {
+                       return set_primary_group_add_callback(module, req);
+               }
+       }
+       return ldb_next_request(module, req);
+}
+
+/*
+ * @brief Module handler for delete operations.
+ *
+ * Currently there is no logging for delete operations.
+ *
+ * @param module The ldb module.
+ * @param req The modify request.
+ *
+ * @return and LDB status code.
+ */
+static int group_delete(
+       struct ldb_module *module,
+       struct ldb_request *req)
+{
+       return ldb_next_request(module, req);
+}
+
+/*
+ * @brief Install the callback function to log a modify request.
+ *
+ * Install the callback function to log a modify request. As we want to log the
+ * returned status code, we need to register a callback function that will be
+ * called once the operation has completed.
+ *
+ * This function reads the current user record so that we can log the before
+ * and after state.
+ *
+ * @param module The ldb module.
+ * @param req The modify request.
+ *
+ * @return and LDB status code.
+ */
+static int set_group_modify_callback(
+       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;
+       struct ldb_result *res = 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;
+       context->log_changes = log_group_membership_changes;
+
+       /*
+        * About to change the group memberships need to read
+        * the current state from the database.
+        */
+       ret = dsdb_module_search_dn(
+               module,
+               context,
+               &res,
+               req->op.add.message->dn,
+               member_attr,
+               DSDB_FLAG_NEXT_MODULE |
+               DSDB_SEARCH_REVEAL_INTERNALS |
+               DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT,
+               NULL);
+       if (ret == LDB_SUCCESS) {
+               context->members = ldb_msg_find_element(res->msgs[0], "member");
+       }
+
+       ret = ldb_build_mod_req(
+               &new_req,
+               ldb,
+               req,
+               req->op.mod.message,
+               req->controls,
+               context,
+               group_audit_callback,
+               req);
+       if (ret != LDB_SUCCESS) {
+               return ret;
+       }
+       return ldb_next_request(module, new_req);
+}
+
+/*
+ * @brief Module handler for modify operations.
+ *
+ * Inspect the current modify request, and if needed log any group membership
+ * changes.
+ *
+ * @param module The ldb module.
+ * @param req The modify request.
+ *
+ * @return and LDB status code.
+ */
+static int group_modify(
+       struct ldb_module *module,
+       struct ldb_request *req)
+{
+
+       struct audit_context *ac =
+               talloc_get_type(
+                       ldb_module_get_private(module),
+                       struct audit_context);
+       /*
+        * Currently we don't log replicated group changes
+        */
+       if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) {
+               return ldb_next_request(module, req);
+       }
+
+       if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL) ||
+           CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) ||
+               (ac->msg_ctx && ac->send_events)) {
+               /*
+                * Avoid the overheads of logging unless it has been
+                * enabled
+                */
+               if (has_group_membership_changes(req)) {
+                       return set_group_modify_callback(module, req);
+               }
+               if (has_primary_group_id(req)) {
+                       return set_primary_group_modify_callback(module, req);
+               }
+       }
+       return ldb_next_request(module, req);
+}
+
+/*
+ * @brief ldb module initialisation
+ *
+ * Initialise the module, loading the private data etc.
+ *
+ * @param module The ldb module to initialise.
+ *
+ * @return An LDB status code.
+ */
+static int group_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);
+
+       context = talloc_zero(module, struct audit_context);
+       if (context == NULL) {
+               return ldb_module_oom(module);
+       }
+
+       if (lp_ctx && lpcfg_dsdb_group_change_notification(lp_ctx)) {
+               context->send_events = true;
+               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_group_audit_log_module_ops = {
+       .name              = "group_audit_log",
+       .add               = group_add,
+       .modify            = group_modify,
+       .del               = group_delete,
+       .init_context      = group_init,
+};
+
+int ldb_group_audit_log_module_init(const char *version)
+{
+       LDB_MODULE_CHECK_VERSION(version);
+       return ldb_register_module(&ldb_group_audit_log_module_ops);
+}
index baa30f9748fdb29a23798af09921d086b0dc2fc9..fa58f19db29a44447298ffae3479a7567550d58a 100644 (file)
@@ -313,6 +313,7 @@ static int samba_dsdb_init(struct ldb_module *module)
                "rdn_name",
                "subtree_delete",
                "repl_meta_data",
+               "group_audit_log",
                "encrypted_secrets",
                "operational",
                "unique_object_sids",
diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.c b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.c
new file mode 100644 (file)
index 0000000..d59da62
--- /dev/null
@@ -0,0 +1,732 @@
+/*
+   Unit tests for the dsdb group auditing code in group_audit.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_group_audit_log_module_init(const char *version);
+#include "../group_audit.c"
+
+#include "lib/ldb/include/ldb_private.h"
+#include <regex.h>
+
+/*
+ * Mock version of dsdb_search_one
+ */
+struct ldb_dn *g_basedn = NULL;
+enum ldb_scope g_scope;
+const char * const *g_attrs = NULL;
+uint32_t g_dsdb_flags;
+const char *g_exp_fmt;
+const char *g_dn = NULL;
+int g_status = LDB_SUCCESS;
+
+int dsdb_search_one(struct ldb_context *ldb,
+                   TALLOC_CTX *mem_ctx,
+                   struct ldb_message **msg,
+                   struct ldb_dn *basedn,
+                   enum ldb_scope scope,
+                   const char * const *attrs,
+                   uint32_t dsdb_flags,
+                   const char *exp_fmt, ...) _PRINTF_ATTRIBUTE(8, 9)
+{
+       struct ldb_dn *dn = ldb_dn_new(mem_ctx, ldb, g_dn);
+       struct ldb_message *m = talloc_zero(mem_ctx, struct ldb_message);
+       m->dn = dn;
+       *msg = m;
+
+       g_basedn = basedn;
+       g_scope = scope;
+       g_attrs = attrs;
+       g_dsdb_flags = dsdb_flags;
+       g_exp_fmt = exp_fmt;
+
+       return g_status;
+}
+
+/*
+ * Mocking for audit_log_hr to capture the called parameters
+ */
+const char *audit_log_hr_prefix = NULL;
+const char *audit_log_hr_message = NULL;
+int audit_log_hr_debug_class = 0;
+int audit_log_hr_debug_level = 0;
+
+static void audit_log_hr_init(void)
+{
+       audit_log_hr_prefix = NULL;
+       audit_log_hr_message = NULL;
+       audit_log_hr_debug_class = 0;
+       audit_log_hr_debug_level = 0;
+}
+
+void audit_log_human_text(
+       const char *prefix,
+       const char *message,
+       int debug_class,
+       int debug_level)
+{
+       audit_log_hr_prefix = prefix;
+       audit_log_hr_message = message;
+       audit_log_hr_debug_class = debug_class;
+       audit_log_hr_debug_level = debug_level;
+}
+
+/*
+ * 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);
+}
+
+/*
+ * 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));
+}
+
+/*
+ * Test helper to insert a transaction_id into a request.
+ */
+static void add_transaction_id(struct ldb_request *req, const char *id)
+{
+       struct GUID guid;
+       struct dsdb_control_transaction_identifier *transaction_id = NULL;
+
+       transaction_id = talloc_zero(
+               req,
+               struct dsdb_control_transaction_identifier);
+       assert_non_null(transaction_id);
+       GUID_from_string(id, &guid);
+       transaction_id->transaction_guid = guid;
+       ldb_request_add_control(
+               req,
+               DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID,
+               false,
+               transaction_id);
+}
+
+/*
+ * Test helper to add a session id and user SID
+ */
+static void add_session_data(
+       TALLOC_CTX *ctx,
+       struct ldb_context *ldb,
+       const char *session,
+       const char *user_sid)
+{
+       struct auth_session_info *sess = NULL;
+       struct security_token *token = NULL;
+       struct dom_sid *sid = NULL;
+       struct GUID session_id;
+
+       sess = talloc_zero(ctx, struct auth_session_info);
+       token = talloc_zero(ctx, struct security_token);
+       sid = talloc_zero(ctx, struct dom_sid);
+       string_to_sid(sid, user_sid);
+       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);
+}
+
+static void test_get_transaction_id(void **state)
+{
+       struct ldb_request *req = NULL;
+       struct GUID *guid;
+       const char * const ID = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       char *guid_str = NULL;
+       struct GUID_txt_buf guid_buff;
+
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+
+       /*
+        * No transaction id, should return a zero guid
+        */
+       req = talloc_zero(ctx, struct ldb_request);
+       guid = get_transaction_id(req);
+       assert_null(guid);
+       TALLOC_FREE(req);
+
+       /*
+        * And now test with the transaction_id set
+        */
+       req = talloc_zero(ctx, struct ldb_request);
+       assert_non_null(req);
+       add_transaction_id(req, ID);
+
+       guid = get_transaction_id(req);
+       guid_str = GUID_buf_string(guid, &guid_buff);
+       assert_string_equal(ID, guid_str);
+       TALLOC_FREE(req);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_audit_group_hr(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+
+       struct tsocket_address *ts = NULL;
+
+       const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+
+       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);
+
+       GUID_from_string(TRANSACTION, &transaction_id);
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts);
+       ldb_set_opaque(ldb, "remoteAddress", ts);
+
+       add_session_data(ctx, ldb, SESSION, SID);
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       add_transaction_id(req, TRANSACTION);
+
+       line = audit_group_human_readable(
+               ctx,
+               module,
+               req,
+               "the-action",
+               "the-user-name",
+               "the-group-name",
+               LDB_ERR_OPERATIONS_ERROR);
+       assert_non_null(line);
+
+       rs =    "\\[the-action\\] at \\["
+               "[^]]*"
+               "\\] status \\[Operations error\\] "
+               "Remote host \\[ipv4:127.0.0.1:0\\] "
+               "SID \\[S-1-5-21-2470180966-3899876309-2637894779\\] "
+               "Group \\[the-group-name\\] "
+               "User \\[the-user-name\\]";
+
+       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);
+
+}
+
+/*
+ * test get_parsed_dns
+ * For this test we assume Valgrind or Address Sanitizer will detect any over
+ * runs. Also we don't care that the values are DN's only that the value in the
+ * element is copied to the parsed_dns.
+ */
+static void test_get_parsed_dns(void **state)
+{
+       struct ldb_message_element *el = NULL;
+       struct parsed_dn *dns = NULL;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       el = talloc_zero(ctx, struct ldb_message_element);
+
+       /*
+        * empty element, zero dns
+        */
+       dns = get_parsed_dns(ctx, el);
+       assert_null(dns);
+
+       /*
+        * one entry
+        */
+       el->num_values = 1;
+       el->values = talloc_zero_array(ctx, DATA_BLOB, 1);
+       el->values[0] = data_blob_string_const("The first value");
+
+       dns = get_parsed_dns(ctx, el);
+
+       assert_ptr_equal(el->values[0].data, dns[0].v->data);
+       assert_int_equal(el->values[0].length, dns[0].v->length);
+
+       TALLOC_FREE(dns);
+       TALLOC_FREE(el);
+
+
+       /*
+        * Multiple values
+        */
+       el = talloc_zero(ctx, struct ldb_message_element);
+       el->num_values = 2;
+       el->values = talloc_zero_array(ctx, DATA_BLOB, 2);
+       el->values[0] = data_blob_string_const("The first value");
+       el->values[0] = data_blob_string_const("The second value");
+
+       dns = get_parsed_dns(ctx, el);
+
+       assert_ptr_equal(el->values[0].data, dns[0].v->data);
+       assert_int_equal(el->values[0].length, dns[0].v->length);
+
+       assert_ptr_equal(el->values[1].data, dns[1].v->data);
+       assert_int_equal(el->values[1].length, dns[1].v->length);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_dn_compare(void **state)
+{
+
+       struct ldb_context *ldb = NULL;
+       struct parsed_dn *a;
+       DATA_BLOB ab;
+
+       struct parsed_dn *b;
+       DATA_BLOB bb;
+
+       int res;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+       const struct GUID *ZERO_GUID = talloc_zero(ctx, struct GUID);
+
+       ldb = ldb_init(ctx, NULL);
+       ldb_register_samba_handlers(ldb);
+
+
+       /*
+        * Identical binary DN's
+        */
+       ab = data_blob_string_const(
+               "<GUID=fbee08fd-6f75-4bd4-af3f-e4f063a6379e>;"
+               "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org");
+       a = talloc_zero(ctx, struct parsed_dn);
+       a->v = &ab;
+
+       bb = data_blob_string_const(
+               "<GUID=fbee08fd-6f75-4bd4-af3f-e4f063a6379e>;"
+               "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org");
+       b = talloc_zero(ctx, struct parsed_dn);
+       b->v = &bb;
+
+       res = dn_compare(ctx, ldb, a, b);
+       assert_int_equal(BINARY_EQUAL, res);
+       /*
+        * DN's should not have been parsed
+        */
+       assert_null(a->dsdb_dn);
+       assert_memory_equal(ZERO_GUID, &a->guid, sizeof(struct GUID));
+       assert_null(b->dsdb_dn);
+       assert_memory_equal(ZERO_GUID, &b->guid, sizeof(struct GUID));
+
+       TALLOC_FREE(a);
+       TALLOC_FREE(b);
+
+       /*
+        * differing binary DN's but equal GUID's
+        */
+       ab = data_blob_string_const(
+               "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651e>;"
+               "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=com");
+       a = talloc_zero(ctx, struct parsed_dn);
+       a->v = &ab;
+
+       bb = data_blob_string_const(
+               "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651e>;"
+               "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org");
+       b = talloc_zero(ctx, struct parsed_dn);
+       b->v = &bb;
+
+       res = dn_compare(ctx, ldb, a, b);
+       assert_int_equal(EQUAL, res);
+       /*
+        * DN's should have been parsed
+        */
+       assert_non_null(a->dsdb_dn);
+       assert_memory_not_equal(ZERO_GUID, &a->guid, sizeof(struct GUID));
+       assert_non_null(b->dsdb_dn);
+       assert_memory_not_equal(ZERO_GUID, &b->guid, sizeof(struct GUID));
+
+       TALLOC_FREE(a);
+       TALLOC_FREE(b);
+
+       /*
+        * differing binary DN's but and second guid greater
+        */
+       ab = data_blob_string_const(
+               "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651d>;"
+               "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=com");
+       a = talloc_zero(ctx, struct parsed_dn);
+       a->v = &ab;
+
+       bb = data_blob_string_const(
+               "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651e>;"
+               "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org");
+       b = talloc_zero(ctx, struct parsed_dn);
+       b->v = &bb;
+
+       res = dn_compare(ctx, ldb, a, b);
+       assert_int_equal(GREATER_THAN, res);
+       /*
+        * DN's should have been parsed
+        */
+       assert_non_null(a->dsdb_dn);
+       assert_memory_not_equal(ZERO_GUID, &a->guid, sizeof(struct GUID));
+       assert_non_null(b->dsdb_dn);
+       assert_memory_not_equal(ZERO_GUID, &b->guid, sizeof(struct GUID));
+
+       TALLOC_FREE(a);
+       TALLOC_FREE(b);
+
+       /*
+        * differing binary DN's but and second guid less
+        */
+       ab = data_blob_string_const(
+               "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651d>;"
+               "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=com");
+       a = talloc_zero(ctx, struct parsed_dn);
+       a->v = &ab;
+
+       bb = data_blob_string_const(
+               "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651c>;"
+               "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org");
+       b = talloc_zero(ctx, struct parsed_dn);
+       b->v = &bb;
+
+       res = dn_compare(ctx, ldb, a, b);
+       assert_int_equal(LESS_THAN, res);
+       /*
+        * DN's should have been parsed
+        */
+       assert_non_null(a->dsdb_dn);
+       assert_memory_not_equal(ZERO_GUID, &a->guid, sizeof(struct GUID));
+       assert_non_null(b->dsdb_dn);
+       assert_memory_not_equal(ZERO_GUID, &b->guid, sizeof(struct GUID));
+
+       TALLOC_FREE(a);
+       TALLOC_FREE(b);
+
+       TALLOC_FREE(ctx);
+}
+
+static void test_get_primary_group_dn(void **state)
+{
+
+       struct ldb_context *ldb = NULL;
+       struct ldb_module *module = NULL;
+       const uint32_t RID = 71;
+       struct dom_sid sid;
+       const char *SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char *DN = "OU=Things,DC=ad,DC=testing,DC=samba,DC=org";
+       const char *dn;
+
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       ldb = ldb_init(ctx, NULL);
+       ldb_register_samba_handlers(ldb);
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+
+       /*
+        * Pass an empty dom sid this will cause dom_sid_split_rid to fail;
+        * assign to sid.num_auths to suppress a valgrind warning.
+        */
+       sid.num_auths = 0;
+       dn = get_primary_group_dn(ctx, module, &sid, RID);
+       assert_null(dn);
+
+       /*
+        * A valid dom sid
+        */
+       assert_true(string_to_sid(&sid, SID));
+       g_dn = DN;
+       dn = get_primary_group_dn(ctx, module, &sid, RID);
+       assert_non_null(dn);
+       assert_string_equal(DN, dn);
+       assert_int_equal(LDB_SCOPE_BASE, g_scope);
+       assert_int_equal(0, g_dsdb_flags);
+       assert_null(g_attrs);
+       assert_null(g_exp_fmt);
+       assert_string_equal
+               ("<SID=S-1-5-21-2470180966-3899876309-71>",
+               ldb_dn_get_extended_linearized(ctx, g_basedn, 1));
+
+       /*
+        * Test dsdb search failure
+        */
+       g_status = LDB_ERR_NO_SUCH_OBJECT;
+       dn = get_primary_group_dn(ctx, module, &sid, RID);
+       assert_null(dn);
+
+       TALLOC_FREE(ldb);
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * Mocking for audit_log_json to capture the called parameters
+ */
+const char *audit_log_json_prefix = NULL;
+struct json_object *audit_log_json_message = NULL;
+int audit_log_json_debug_class = 0;
+int audit_log_json_debug_level = 0;
+
+static void audit_log_json_init(void)
+{
+       audit_log_json_prefix = NULL;
+       audit_log_json_message = NULL;
+       audit_log_json_debug_class = 0;
+       audit_log_json_debug_level = 0;
+}
+
+void audit_log_json(
+       const char* prefix,
+       struct json_object* message,
+       int debug_class,
+       int debug_level)
+{
+       audit_log_json_prefix = prefix;
+       audit_log_json_message = message;
+       audit_log_json_debug_class = debug_class;
+       audit_log_json_debug_level = debug_level;
+}
+
+/*
+ * Mocking for audit_message_send to capture the called parameters
+ */
+struct imessaging_context *audit_message_send_msg_ctx = NULL;
+const char *audit_message_send_server_name = NULL;
+uint32_t audit_message_send_message_type = 0;
+struct json_object *audit_message_send_message = NULL;
+
+static void audit_message_send_init(void) {
+       audit_message_send_msg_ctx = NULL;
+       audit_message_send_server_name = NULL;
+       audit_message_send_message_type = 0;
+       audit_message_send_message = NULL;
+}
+void audit_message_send(
+       struct imessaging_context *msg_ctx,
+       const char *server_name,
+       uint32_t message_type,
+       struct json_object *message)
+{
+       audit_message_send_msg_ctx = msg_ctx;
+       audit_message_send_server_name = server_name;
+       audit_message_send_message_type = message_type;
+       audit_message_send_message = message;
+}
+
+static void test_audit_group_json(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       struct ldb_request *req = NULL;
+
+       struct tsocket_address *ts = NULL;
+
+       const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+       struct GUID transaction_id;
+       const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+
+
+       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);
+
+       GUID_from_string(TRANSACTION, &transaction_id);
+
+       module = talloc_zero(ctx, struct ldb_module);
+       module->ldb = ldb;
+
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts);
+       ldb_set_opaque(ldb, "remoteAddress", ts);
+
+       add_session_data(ctx, ldb, SESSION, SID);
+
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       add_transaction_id(req, TRANSACTION);
+
+       before = time(NULL);
+       json = audit_group_json(
+               module,
+               req,
+               "the-action",
+               "the-user-name",
+               "the-group-name",
+               LDB_ERR_OPERATIONS_ERROR);
+       assert_int_equal(3, json_object_size(json.root));
+
+       v = json_object_get(json.root, "type");
+       assert_non_null(v);
+       assert_string_equal("groupChange", 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, "groupChange");
+       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, AUDIT_MAJOR, AUDIT_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, "user");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("the-user-name", json_string_value(v));
+
+       v = json_object_get(audit, "group");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("the-group-name", json_string_value(v));
+
+       v = json_object_get(audit, "action");
+       assert_non_null(v);
+       assert_true(json_is_string(v));
+       assert_string_equal("the-action", json_string_value(v));
+
+       json_free(&json);
+       TALLOC_FREE(ctx);
+
+}
+
+static void test_place_holder(void **state)
+{
+       audit_log_json_init();
+       audit_log_hr_init();
+       audit_message_send_init();
+}
+
+/*
+ * Note: to run under valgrind us:
+ *       valgrind --suppressions=test_group_audit.valgrind bin/test_group_audit
+ *       This suppresses the errors generated because the ldb_modules are not
+ *       de-registered.
+ *
+ */
+int main(void) {
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test(test_audit_group_json),
+               cmocka_unit_test(test_place_holder),
+               cmocka_unit_test(test_get_transaction_id),
+               cmocka_unit_test(test_audit_group_hr),
+               cmocka_unit_test(test_get_parsed_dns),
+               cmocka_unit_test(test_dn_compare),
+               cmocka_unit_test(test_get_primary_group_dn),
+
+       };
+
+       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_group_audit.valgrind b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.valgrind
new file mode 100644 (file)
index 0000000..1cf2b4e
--- /dev/null
@@ -0,0 +1,19 @@
+{
+   ldb_modules_load modules not are freed
+   Memcheck:Leak
+   match-leak-kinds: possible
+   fun:malloc
+   fun:__talloc_with_prefix
+   fun:__talloc
+   fun:_talloc_named_const
+   fun:talloc_named_const
+   fun:ldb_register_module
+   fun:ldb_init_module
+   fun:ldb_modules_load_path
+   fun:ldb_modules_load_dir
+   fun:ldb_modules_load_path
+   fun:ldb_modules_load
+   fun:ldb_init
+}
+
+
index 3e591a0a0d57f0af13b3f799dbc62e909735002e..1216a1fd99fcfbaaf813dde468bcd4bbcc0cf298 100644 (file)
@@ -40,6 +40,7 @@ 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.
@@ -69,6 +70,18 @@ if bld.CONFIG_GET('ENABLE_SELFTEST'):
                 DSDB_MODULE_HELPERS
             ''',
             install=False)
+    bld.SAMBA_BINARY('test_group_audit',
+            source='tests/test_group_audit.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 6c821fb7d7bbab2344ec6281caee75aa69186569..e5c503239dfd99a76184e7399c68c10fad6b27d7 100644 (file)
@@ -441,3 +441,19 @@ bld.SAMBA_MODULE('ldb_audit_log',
             samdb
         '''
        )
+
+bld.SAMBA_MODULE('ldb_group_audit_log',
+       source='group_audit.c',
+       subsystem='ldb',
+       init_function='ldb_group_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 a95352fa22fe71e4a42274ea01c05d77e74446d6..8b1fb7b280ae41e695bee51c9258551140b71159 100755 (executable)
@@ -693,6 +693,10 @@ 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.group_audit",
+                           extra_args=['-U"$USERNAME%$PASSWORD"'],
+                           environ={'CLIENT_IP': '127.0.0.11',
+                                    'SOCKET_WRAPPER_DEFAULT_IFACE': 11})
 
 planoldpythontestsuite("fl2008r2dc:local",
                        "samba.tests.getdcname",