[PATCH] Audit logging of DSDB operations, password changes and group membership changes.

Gary Lockyer gary at catalyst.net.nz
Wed May 30 23:24:16 UTC 2018


Patches to 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.
      * Group membership changes
      * User primary group changes.

Review and push appreciated.

Thanks
Gary

-------------- next part --------------
From 9aff380448ffeed779af76d11f7e5cfe0de8b12c Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Thu, 17 May 2018 08:03:00 +1200
Subject: [PATCH 1/9] lib audit_logging: re-factor and add functions.

Re-factor the common calls to json_dumps DEBUGC and audit_message_send
into a separate function.
Add functions to retrieve json object and json array elements

Signed-off-by: Gary Lockyer <gary at catalyst.net.nz>
---
 auth/auth_log.c                              |  22 +----
 lib/audit_logging/audit_logging.c            | 115 +++++++++++++++++++++-
 lib/audit_logging/audit_logging.h            |  24 ++++-
 lib/audit_logging/tests/audit_logging_test.c | 137 +++++++++++++++++++++++++++
 4 files changed, 269 insertions(+), 29 deletions(-)

diff --git a/auth/auth_log.c b/auth/auth_log.c
index 87daf2f..369a5c9 100644
--- a/auth/auth_log.c
+++ b/auth/auth_log.c
@@ -82,31 +82,13 @@ static void log_json(struct imessaging_context *msg_ctx,
 		     int debug_class,
 		     int debug_level)
 {
-	char* json = NULL;
-
-	if (object->error) {
-		return;
-	}
-
-	json = json_dumps(object->root, 0);
-	if (json == NULL) {
-		DBG_ERR("Unable to convert JSON object to string\n");
-		object->error = true;
-		return;
-	}
-
-	DEBUGC(debug_class, debug_level, ("JSON %s: %s\n", type, json));
+	audit_log_json(type, object, debug_class, debug_level);
 	if (msg_ctx && lp_ctx && lpcfg_auth_event_notification(lp_ctx)) {
 		audit_message_send(msg_ctx,
 				   AUTH_EVENT_NAME,
 				   MSG_AUTH_LOG,
-				   json);
-	}
-
-	if (json) {
-		free(json);
+				   object);
 	}
-
 }
 
 /*
diff --git a/lib/audit_logging/audit_logging.c b/lib/audit_logging/audit_logging.c
index 5c16806..bb63205 100644
--- a/lib/audit_logging/audit_logging.c
+++ b/lib/audit_logging/audit_logging.c
@@ -102,9 +102,47 @@ char* audit_get_timestamp(TALLOC_CTX *frame)
 	return ts;
 }
 
-#ifdef HAVE_JANSSON
+/*
+ * @brief write an audit message to the audit logs.
+ *
+ * Write the audit message to the samba logs.
+ *
+ * @param prefix Text to be printed at the start of the log line
+ * @param message The content of the log line.
+ * @param debub_class The debug class to log the message with.
+ * @param debug_level The debug level to log the message with.
+ */
+void audit_log_hr(
+	const char* prefix,
+	const char* message,
+	int debug_class,
+	int debug_level)
+{
+	DEBUGC(debug_class, debug_level, ("%s %s\n", prefix, message));
+}
 
-#include "system/time.h"
+#ifdef HAVE_JANSSON
+/*
+ * @brief write a json object to the samba audit logs.
+ *
+ * Write the json object to the audit logs as a formatted string
+ *
+ * @param prefix Text to be printed at the start of the log line
+ * @param message The content of the log line.
+ * @param debub_class The debug class to log the message with.
+ * @param debug_level The debug level to log the message with.
+ */
+void audit_log_json(
+	const char* prefix,
+	struct json_object* message,
+	int debug_class,
+	int debug_level)
+{
+	TALLOC_CTX *ctx = talloc_new(NULL);
+	char *s = json_to_string(ctx, message);
+	DEBUGC(debug_class, debug_level, ("JSON %s: %s\n", prefix, s));
+	TALLOC_FREE(ctx);
+}
 
 /*
  * @brief get a connection to the messaging event server.
@@ -192,14 +230,18 @@ void audit_message_send(
 	struct imessaging_context *msg_ctx,
 	const char *server_name,
 	uint32_t message_type,
-	const char *message)
+	struct json_object *message)
 {
 	struct server_id event_server;
 	NTSTATUS status;
-	DATA_BLOB message_blob = data_blob_string_const(message);
+
+	const char *message_string = NULL;
+	DATA_BLOB message_blob = data_blob_null;
+	TALLOC_CTX *ctx = talloc_new(NULL);
 
 	if (msg_ctx == NULL) {
 		DBG_DEBUG("No messaging context\n");
+		TALLOC_FREE(ctx);
 		return;
 	}
 
@@ -213,9 +255,12 @@ void audit_message_send(
 		DBG_ERR("get_event_server for %s returned (%s)\n",
 			server_name,
 			nt_errstr(status));
+		TALLOC_FREE(ctx);
 		return;
 	}
 
+	message_string = json_to_string(ctx, message);
+	message_blob = data_blob_string_const(message_string);
 	status = imessaging_send(
 		msg_ctx,
 		event_server,
@@ -232,6 +277,7 @@ void audit_message_send(
 			DBG_ERR("get_event_server for %s returned (%s)\n",
 				server_name,
 				nt_errstr(status));
+			TALLOC_FREE(ctx);
 			return;
 		}
 		imessaging_send(
@@ -240,6 +286,7 @@ void audit_message_send(
 			message_type,
 			&message_blob);
 	}
+	TALLOC_FREE(ctx);
 }
 
 /*
@@ -768,4 +815,64 @@ char *json_to_string(TALLOC_CTX *mem_ctx, struct json_object *object)
 
 	return json_string;
 }
+
+/*
+ * @brief get a json array named "name" from the json object.
+ *
+ * Get the array attribute named name, creating it if it does not exist.
+ *
+ * @param object the json object.
+ * @param name the name of the array attribute
+ *
+ * @return The array object, will be created if it did not exist.
+ */
+struct json_object json_get_array(struct json_object *object, const char* name)
+{
+
+	struct json_object array = json_new_array();
+	json_t *a = NULL;
+
+	if (object->error) {
+		array.error = true;
+		return array;
+	}
+
+	a = json_object_get(object->root, name);
+	if (a == NULL) {
+		return array;
+	}
+	json_array_extend(array.root, a);
+
+	return array;
+}
+
+/*
+ * @brief get a json object named "name" from the json object.
+ *
+ * Get the object attribute named name, creating it if it does not exist.
+ *
+ * @param object the json object.
+ * @param name the name of the object attribute
+ *
+ * @return The object, will be created if it did not exist.
+ */
+struct json_object json_get_object(struct json_object *object, const char* name)
+{
+
+	struct json_object o = json_new_object();
+	json_t *v = NULL;
+
+	if (object->error) {
+		o.error = true;
+		return o;
+	}
+
+	v = json_object_get(object->root, name);
+	if (v == NULL) {
+		return o;
+	}
+	json_object_update(o.root, v);
+
+	return o;
+}
 #endif
diff --git a/lib/audit_logging/audit_logging.h b/lib/audit_logging/audit_logging.h
index 763f3ed..6f493d8 100644
--- a/lib/audit_logging/audit_logging.h
+++ b/lib/audit_logging/audit_logging.h
@@ -23,12 +23,12 @@
 
 char* audit_get_timestamp(
 	TALLOC_CTX *frame);
+void audit_log_hr(
+	const char *prefix,
+	const char *message,
+	int debug_class,
+	int debug_level);
 
-void audit_message_send(
-	struct imessaging_context *msg_ctx,
-	const char *server_name,
-	uint32_t message_type,
-	const char *message);
 #ifdef HAVE_JANSSON
 #include <jansson.h>
 /*
@@ -40,6 +40,16 @@ struct json_object {
 	bool error;
 };
 
+void audit_log_json(
+	const char *prefix,
+	struct json_object *message,
+	int debug_class,
+	int debug_level);
+void audit_message_send(
+	struct imessaging_context *msg_ctx,
+	const char *server_name,
+	uint32_t message_type,
+	struct json_object *message);
 struct json_object json_new_object(void);
 struct json_object json_new_array(void);
 void json_free(struct json_object *object);
@@ -85,5 +95,9 @@ void json_add_guid(
 	const char *name,
 	const struct GUID *guid);
 
+struct json_object json_get_array(struct json_object *object, const char* name);
+struct json_object json_get_object(
+	struct json_object *object,
+	const char* name);
 char *json_to_string(TALLOC_CTX *mem_ctx, struct json_object *object);
 #endif
diff --git a/lib/audit_logging/tests/audit_logging_test.c b/lib/audit_logging/tests/audit_logging_test.c
index 8385e9c..aba35e8 100644
--- a/lib/audit_logging/tests/audit_logging_test.c
+++ b/lib/audit_logging/tests/audit_logging_test.c
@@ -490,6 +490,141 @@ static void test_json_to_string(void **state)
 	json_free(&object);
 	TALLOC_FREE(ctx);
 }
+
+static void test_json_get_array(void **state)
+{
+	struct json_object object;
+	struct json_object array;
+	struct json_object stored_array = json_new_array();
+	json_t *value = NULL;
+	json_t *o = NULL;
+	struct json_object o1;
+	struct json_object o2;
+
+	object = json_new_object();
+
+	array = json_get_array(&object, "not-there");
+	assert_false(array.error);
+	assert_non_null(array.root);
+	assert_true(json_is_array(array.root));
+	json_free(&array);
+
+	o1 = json_new_object();
+	json_add_string(&o1, "value", "value-one");
+	json_add_object(&stored_array, NULL, &o1);
+	json_add_object(&object, "stored_array", &stored_array);
+
+	array = json_get_array(&object, "stored_array");
+	assert_false(array.error);
+	assert_non_null(array.root);
+	assert_true(json_is_array(array.root));
+
+	assert_int_equal(1, json_array_size(array.root));
+
+	o = json_array_get(array.root, 0);
+	assert_non_null(o);
+	assert_true(json_is_object(o));
+
+	value = json_object_get(o, "value");
+	assert_non_null(value);
+	assert_true(json_is_string(value));
+
+	assert_string_equal("value-one", json_string_value(value));
+	json_free(&array);
+
+	/*
+	 * Now update the array and add it back to the object
+	 */
+	array = json_get_array(&object, "stored_array");
+	assert_true(json_is_array(array.root));
+	o2 = json_new_object();
+	json_add_string(&o2, "value", "value-two");
+	assert_false(o2.error);
+	json_add_object(&array, NULL, &o2);
+	assert_true(json_is_array(array.root));
+	json_add_object(&object, "stored_array", &array);
+	assert_true(json_is_array(array.root));
+
+	array = json_get_array(&object, "stored_array");
+	assert_non_null(array.root);
+	assert_true(json_is_array(array.root));
+	assert_false(array.error);
+	assert_true(json_is_array(array.root));
+
+	assert_int_equal(2, json_array_size(array.root));
+
+	o = json_array_get(array.root, 0);
+	assert_non_null(o);
+	assert_true(json_is_object(o));
+
+	assert_non_null(value);
+	assert_true(json_is_string(value));
+
+	assert_string_equal("value-one", json_string_value(value));
+
+	o = json_array_get(array.root, 1);
+	assert_non_null(o);
+	assert_true(json_is_object(o));
+
+	value = json_object_get(o, "value");
+	assert_non_null(value);
+	assert_true(json_is_string(value));
+
+	assert_string_equal("value-two", json_string_value(value));
+
+	json_free(&array);
+	json_free(&object);
+}
+
+static void test_json_get_object(void **state)
+{
+	struct json_object object;
+	struct json_object o1;
+	struct json_object o2;
+	struct json_object o3;
+	json_t *value = NULL;
+
+	object = json_new_object();
+
+	o1 = json_get_object(&object, "not-there");
+	assert_false(o1.error);
+	assert_non_null(o1.root);
+	assert_true(json_is_object(o1.root));
+	json_free(&o1);
+
+	o1 = json_new_object();
+	json_add_string(&o1, "value", "value-one");
+	json_add_object(&object, "stored_object", &o1);
+
+	o2 = json_get_object(&object, "stored_object");
+	assert_false(o2.error);
+	assert_non_null(o2.root);
+	assert_true(json_is_object(o2.root));
+
+	value = json_object_get(o2.root, "value");
+	assert_non_null(value);
+	assert_true(json_is_string(value));
+
+	assert_string_equal("value-one", json_string_value(value));
+
+	json_add_string(&o2, "value", "value-two");
+	json_add_object(&object, "stored_object", &o2);
+
+
+	o3 = json_get_object(&object, "stored_object");
+	assert_false(o3.error);
+	assert_non_null(o3.root);
+	assert_true(json_is_object(o3.root));
+
+	value = json_object_get(o3.root, "value");
+	assert_non_null(value);
+	assert_true(json_is_string(value));
+
+	assert_string_equal("value-two", json_string_value(value));
+
+	json_free(&o3);
+	json_free(&object);
+}
 #endif
 
 static void test_audit_get_timestamp(void **state)
@@ -549,6 +684,8 @@ int main(int argc, const char **argv)
 		cmocka_unit_test(test_json_add_sid),
 		cmocka_unit_test(test_json_add_guid),
 		cmocka_unit_test(test_json_to_string),
+		cmocka_unit_test(test_json_get_array),
+		cmocka_unit_test(test_json_get_object),
 #endif
 		cmocka_unit_test(test_audit_get_timestamp),
 	};
-- 
2.7.4


From b24508c35ced1eace9152c22886b43a0d625112c Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Fri, 25 May 2018 15:21:33 +1200
Subject: [PATCH 2/9] auth tests: irpc remove "auth_event" name on completion

Remove the "auth_event" name on completion of tests to prevent issues
with tests using messaging.

Signed-off-by: Gary Lockyer <gary at catalyst.net.nz>
---
 python/samba/tests/auth_log_base.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/python/samba/tests/auth_log_base.py b/python/samba/tests/auth_log_base.py
index 5a70ce3..6edd0f6 100644
--- a/python/samba/tests/auth_log_base.py
+++ b/python/samba/tests/auth_log_base.py
@@ -58,6 +58,7 @@ class AuthLogTestBase(samba.tests.TestCase):
         if self.msg_handler_and_context:
             self.msg_ctx.deregister(self.msg_handler_and_context,
                                     msg_type=MSG_AUTH_LOG)
+            self.msg_ctx.irpc_remove_name(AUTH_EVENT_NAME)
 
     def waitForMessages(self, isLastExpectedMessage, connection=None):
         """Wait for all the expected messages to arrive
-- 
2.7.4


From a9bd07336c9ce6b69b75e3be5b8738425126fc5e Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Wed, 4 Apr 2018 12:38:25 +1200
Subject: [PATCH 3/9] cldap: clear remote address after cldap_dse_fill

Need to clear the remote address as the ldb handle is shared, and
changes made by internal processes would be logged as coming from the
last cldap requester

Signed-off-by: Gary Lockyer <gary at catalyst.net.nz>
---
 source4/cldap_server/rootdse.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/source4/cldap_server/rootdse.c b/source4/cldap_server/rootdse.c
index 3f389ce..a5e1c6b 100644
--- a/source4/cldap_server/rootdse.c
+++ b/source4/cldap_server/rootdse.c
@@ -166,6 +166,13 @@ void cldapd_rootdse_request(struct cldap_socket *cldap,
 	cldapd_rootdse_fill(cldapd, tmp_ctx, search, &reply.response,
 			    reply.result);
 
+	/*
+	 * We clear this after cldapd_rootdse_fill as this is shared ldb
+	 * and if it was not cleared the audit logging would report changes
+	 * made by internal processes as coming from the last cldap requester
+	 */
+	ldb_set_opaque(cldapd->samctx, "remoteAddress", NULL);
+
 	status = cldap_reply_send(cldap, &reply);
 	if (!NT_STATUS_IS_OK(status)) {
 		DEBUG(2,("cldap rootdse query failed '%s' - %s\n",
-- 
2.7.4


From 0ce43adad1e0527505f8347f7f005a1514aab5e3 Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Fri, 25 May 2018 09:53:29 +1200
Subject: [PATCH 4/9] dsdb acl: Copy dsdb_control_password_acl_validation into
 reply

Copy the dsdb_control_password_acl_validation into the reply so that it
is available to the audit_logging module.  The audit logging module
uses it to differentiate between password change and reset operations.

We include it in the result for failed request to allow the logging of
failed attempts.

Signed-off-by: Gary Lockyer <gary at catalyst.net.nz>
---
 source4/dsdb/samdb/ldb_modules/acl.c | 187 +++++++++++++++++++++++++++++++++--
 1 file changed, 177 insertions(+), 10 deletions(-)

diff --git a/source4/dsdb/samdb/ldb_modules/acl.c b/source4/dsdb/samdb/ldb_modules/acl.c
index 8b1dcbe..cd7144a 100644
--- a/source4/dsdb/samdb/ldb_modules/acl.c
+++ b/source4/dsdb/samdb/ldb_modules/acl.c
@@ -968,13 +968,15 @@ static int acl_check_self_membership(TALLOC_CTX *mem_ctx,
 	return ret;
 }
 
-static int acl_check_password_rights(TALLOC_CTX *mem_ctx,
-				     struct ldb_module *module,
-				     struct ldb_request *req,
-				     struct security_descriptor *sd,
-				     struct dom_sid *sid,
-				     const struct dsdb_class *objectclass,
-				     bool userPassword)
+static int acl_check_password_rights(
+	TALLOC_CTX *mem_ctx,
+	struct ldb_module *module,
+	struct ldb_request *req,
+	struct security_descriptor *sd,
+	struct dom_sid *sid,
+	const struct dsdb_class *objectclass,
+	bool userPassword,
+	struct  dsdb_control_password_acl_validation **control_for_response)
 {
 	int ret = LDB_SUCCESS;
 	unsigned int del_attr_cnt = 0, add_attr_cnt = 0, rep_attr_cnt = 0;
@@ -996,6 +998,12 @@ static int acl_check_password_rights(TALLOC_CTX *mem_ctx,
 		talloc_free(tmp_ctx);
 		return LDB_ERR_OPERATIONS_ERROR;
 	}
+	/*
+	 * Set control_for_response to pav so it can be added to the response
+	 * and be passed up to the audit_log module which uses it to identify
+	 * password reset attempts.
+	 */
+	*control_for_response = pav;
 
 	c = ldb_request_get_control(req, DSDB_CONTROL_PASSWORD_CHANGE_OID);
 	if (c != NULL) {
@@ -1165,6 +1173,105 @@ checked:
 	return LDB_SUCCESS;
 }
 
+/*
+ * Context needed by acl_callback
+ */
+struct acl_callback_context {
+	struct ldb_request *request;
+	struct ldb_module *module;
+};
+
+/*
+ * @brief Copy the password validation control to the reply.
+ *
+ * Copy the dsdb_control_password_acl_validation control from the request,
+ * to the reply.  The control is used by the audit_log module to identify
+ * password rests.
+ *
+ * @param req the ldb request.
+ * @param ares the result, updated with the control.
+ */
+static void copy_password_acl_validation_control(
+	struct ldb_request *req,
+	struct ldb_reply *ares)
+{
+	struct ldb_control *pav_ctrl = NULL;
+	struct dsdb_control_password_acl_validation *pav = NULL;
+
+	pav_ctrl = ldb_request_get_control(
+		discard_const(req),
+		DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID);
+	if (pav_ctrl == NULL) {
+		return;
+	}
+
+	pav = talloc_get_type_abort(
+		pav_ctrl->data,
+		struct dsdb_control_password_acl_validation);
+	if (pav == NULL) {
+		return;
+	}
+	ldb_reply_add_control(
+		ares,
+		DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID,
+		false,
+		pav);
+}
+/*
+ * @brief call back function for acl_modify.
+ *
+ * Calls acl_copy to copy the dsdb_control_password_acl_validation from
+ * the request to the reply.
+ *
+ * @param req the ldb_request.
+ * @param ares the operation result.
+ *
+ * @return the LDB_STATUS
+ */
+static int acl_callback(struct ldb_request *req, struct ldb_reply *ares)
+{
+	struct acl_callback_context *ac = NULL;
+
+	ac = talloc_get_type(req->context, struct acl_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:
+		/*
+		 * Copy the ACL control from the request to the response
+		 */
+		copy_password_acl_validation_control(req, ares);
+		return ldb_module_done(
+			ac->request,
+			ares->controls,
+			ares->response,
+			ares->error);
+
+	default:
+		/* Can't happen */
+		return LDB_ERR_OPERATIONS_ERROR;
+	}
+}
+
 static int acl_modify(struct ldb_module *module, struct ldb_request *req)
 {
 	int ret;
@@ -1187,6 +1294,10 @@ static int acl_modify(struct ldb_module *module, struct ldb_request *req)
 		"objectSid",
 		NULL
 	};
+	struct acl_callback_context *context = NULL;
+	struct ldb_request *new_req = NULL;
+	struct  dsdb_control_password_acl_validation *pav = NULL;
+	struct ldb_control **controls = NULL;
 
 	if (ldb_dn_is_special(msg->dn)) {
 		return ldb_next_request(module, req);
@@ -1324,6 +1435,14 @@ static int acl_modify(struct ldb_module *module, struct ldb_request *req)
 		} else if (ldb_attr_cmp("unicodePwd", el->name) == 0 ||
 			   (userPassword && ldb_attr_cmp("userPassword", el->name) == 0) ||
 			   ldb_attr_cmp("clearTextPassword", el->name) == 0) {
+			/*
+			 * Ideally we would do the acl_check_password_rights
+			 * before we checked the other attributes, i.e. in a
+			 * loop before the current one.
+			 * Have not done this as yet in order to limit the size
+			 * of the change. To limit the possibility of breaking
+			 * the ACL logic.
+			 */
 			if (password_rights_checked) {
 				continue;
 			}
@@ -1333,7 +1452,8 @@ static int acl_modify(struct ldb_module *module, struct ldb_request *req)
 							sd,
 							sid,
 							objectclass,
-							userPassword);
+							userPassword,
+							&pav);
 			if (ret != LDB_SUCCESS) {
 				goto fail;
 			}
@@ -1382,10 +1502,57 @@ static int acl_modify(struct ldb_module *module, struct ldb_request *req)
 
 success:
 	talloc_free(tmp_ctx);
-	return ldb_next_request(module, req);
+	context = talloc_zero(req, struct acl_callback_context);
+
+	if (context == NULL) {
+		return ldb_oom(ldb);
+	}
+	context->request = req;
+	context->module  = module;
+	ret = ldb_build_mod_req(
+		&new_req,
+		ldb,
+		req,
+		req->op.mod.message,
+		req->controls,
+		context,
+		acl_callback,
+		req);
+	if (ret != LDB_SUCCESS) {
+		return ret;
+	}
+	return ldb_next_request(module, new_req);
 fail:
 	talloc_free(tmp_ctx);
-	return ret;
+	/*
+	 * We copy the pav into the result, so that the password reset
+	 * logging code in audit_log can log failed password reset attempts.
+	 */
+	if (pav) {
+		struct ldb_control *control = NULL;
+
+		controls = talloc_zero_array(req, struct ldb_control *, 2);
+		if (controls == NULL) {
+			return ldb_oom(ldb);
+		}
+
+		control = talloc(controls, struct ldb_control);
+
+		if (control == NULL) {
+			return ldb_oom(ldb);
+		}
+
+		control->oid= talloc_strdup(
+			control,
+			DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID);
+		if (control->oid == NULL) {
+			return ldb_oom(ldb);
+		}
+		control->critical	= false;
+		control->data	= pav;
+		*controls = control;
+	}
+	return ldb_module_done(req, controls, NULL, ret);
 }
 
 /* similar to the modify for the time being.
-- 
2.7.4


From bf2667e828f0c851b9aa8de8b6561685edd191da Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Wed, 30 May 2018 14:43:25 +1200
Subject: [PATCH 5/9] rpc_server: common routine to open ldb in system session

Add a function to open an ldb connection under the system session and
save the remote users session details in a ldb_opaque.  This will allow
the audit logging to log the original session for operations performed
in the system session.

Signed-off-by: Gary Lockyer <gary at catalyst.net.nz>
---
 source4/rpc_server/common/server_info.c | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/source4/rpc_server/common/server_info.c b/source4/rpc_server/common/server_info.c
index 0aabcda..3532a6d 100644
--- a/source4/rpc_server/common/server_info.c
+++ b/source4/rpc_server/common/server_info.c
@@ -186,3 +186,27 @@ bool dcesrv_common_validate_share_name(TALLOC_CTX *mem_ctx, const char *share_na
 
 	return true;
 }
+
+/*
+ * Open a ldb connection as the System user.
+ */
+struct ldb_context *connect_as_system(
+	TALLOC_CTX *mem_ctx,
+	struct dcesrv_call_state *dce_call)
+{
+	struct ldb_context *samdb = NULL;
+	samdb = samdb_connect(
+		mem_ctx,
+		dce_call->event_ctx,
+		dce_call->conn->dce_ctx->lp_ctx,
+		system_session(dce_call->conn->dce_ctx->lp_ctx),
+		dce_call->conn->remote_address,
+		0);
+	if (samdb) {
+		ldb_set_opaque(
+			samdb,
+			"networkSessionInfo",
+			dce_call->conn->auth_state.session_info);
+	}
+	return samdb;
+}
-- 
2.7.4


From 9f7420fb22f8ad893dc716c7d3e8a6822dc5a60c Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Wed, 30 May 2018 14:44:19 +1200
Subject: [PATCH 6/9] rpc_server lsa: pass remote connection data

Ensure that the session details of the requesting user are available to
the audit logging module for the CreateSecret and OpenSecret operations.

Signed-off-by: Gary Lockyer <gary at catalyst.net.nz>
---
 source4/rpc_server/lsa/dcesrv_lsa.c | 18 ++----------------
 1 file changed, 2 insertions(+), 16 deletions(-)

diff --git a/source4/rpc_server/lsa/dcesrv_lsa.c b/source4/rpc_server/lsa/dcesrv_lsa.c
index 8c540ab..ad455e4 100644
--- a/source4/rpc_server/lsa/dcesrv_lsa.c
+++ b/source4/rpc_server/lsa/dcesrv_lsa.c
@@ -3167,8 +3167,6 @@ static NTSTATUS dcesrv_lsa_SetSystemAccessAccount(struct dcesrv_call_state *dce_
 {
 	DCESRV_FAULT(DCERPC_FAULT_OP_RNG_ERROR);
 }
-
-
 /*
   lsa_CreateSecret
 */
@@ -3234,13 +3232,7 @@ static NTSTATUS dcesrv_lsa_CreateSecret(struct dcesrv_call_state *dce_call, TALL
 		/* We need to connect to the database as system, as this is one
 		 * of the rare RPC calls that must read the secrets (and this
 		 * is denied otherwise) */
-		samdb = samdb_connect(
-			mem_ctx,
-			dce_call->event_ctx,
-			dce_call->conn->dce_ctx->lp_ctx,
-			system_session(dce_call->conn->dce_ctx->lp_ctx),
-			dce_call->conn->remote_address,
-			0);
+		samdb = connect_as_system(mem_ctx, dce_call);
 		secret_state->sam_ldb = talloc_reference(secret_state, samdb);
 		NT_STATUS_HAVE_NO_MEMORY(secret_state->sam_ldb);
 
@@ -3380,13 +3372,7 @@ static NTSTATUS dcesrv_lsa_OpenSecret(struct dcesrv_call_state *dce_call, TALLOC
 	if (strncmp("G$", r->in.name.string, 2) == 0) {
 		name = &r->in.name.string[2];
 		/* We need to connect to the database as system, as this is one of the rare RPC calls that must read the secrets (and this is denied otherwise) */
-		samdb = samdb_connect(
-			mem_ctx,
-			dce_call->event_ctx,
-			dce_call->conn->dce_ctx->lp_ctx,
-			system_session(dce_call->conn->dce_ctx->lp_ctx),
-			dce_call->conn->remote_address,
-			0);
+		samdb = connect_as_system(mem_ctx, dce_call);
 		secret_state->sam_ldb = talloc_reference(secret_state, samdb);
 		secret_state->global = true;
 
-- 
2.7.4


From 9e7b82d3a38efb5b34a379d4e7cd670fbcad5a26 Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Wed, 30 May 2018 14:45:03 +1200
Subject: [PATCH 7/9] rpc_server backupkey: pass remote connection data

Ensure that the requesting session data is passed to the audit logging
module for BackupKey requests.

Signed-off-by: Gary Lockyer <gary at catalyst.net.nz>
---
 source4/rpc_server/backupkey/dcesrv_backupkey.c         | 8 ++------
 source4/rpc_server/backupkey/dcesrv_backupkey_heimdal.c | 9 ++-------
 source4/rpc_server/wscript_build                        | 2 +-
 3 files changed, 5 insertions(+), 14 deletions(-)

diff --git a/source4/rpc_server/backupkey/dcesrv_backupkey.c b/source4/rpc_server/backupkey/dcesrv_backupkey.c
index d2c3f2a..ce3cb9a 100644
--- a/source4/rpc_server/backupkey/dcesrv_backupkey.c
+++ b/source4/rpc_server/backupkey/dcesrv_backupkey.c
@@ -22,6 +22,7 @@
 
 #include "includes.h"
 #include "rpc_server/dcerpc_server.h"
+#include "rpc_server/common/common.h"
 #include "librpc/gen_ndr/ndr_backupkey.h"
 #include "dsdb/common/util.h"
 #include "dsdb/samdb/samdb.h"
@@ -1774,12 +1775,7 @@ static WERROR dcesrv_bkrp_BackupKey(struct dcesrv_call_state *dce_call,
 		return WERR_NOT_SUPPORTED;
 	}
 
-	ldb_ctx = samdb_connect(mem_ctx,
-				dce_call->event_ctx,
-				dce_call->conn->dce_ctx->lp_ctx,
-				system_session(dce_call->conn->dce_ctx->lp_ctx),
-				dce_call->conn->remote_address,
-				0);
+	ldb_ctx = connect_as_system(mem_ctx, dce_call);
 
 	if (samdb_rodc(ldb_ctx, &is_rodc) != LDB_SUCCESS) {
 		talloc_unlink(mem_ctx, ldb_ctx);
diff --git a/source4/rpc_server/backupkey/dcesrv_backupkey_heimdal.c b/source4/rpc_server/backupkey/dcesrv_backupkey_heimdal.c
index d2fc480..8b650c9 100644
--- a/source4/rpc_server/backupkey/dcesrv_backupkey_heimdal.c
+++ b/source4/rpc_server/backupkey/dcesrv_backupkey_heimdal.c
@@ -21,6 +21,7 @@
 
 #include "includes.h"
 #include "rpc_server/dcerpc_server.h"
+#include "rpc_server/common/common.h"
 #include "librpc/gen_ndr/ndr_backupkey.h"
 #include "dsdb/common/util.h"
 #include "dsdb/samdb/samdb.h"
@@ -1814,13 +1815,7 @@ static WERROR dcesrv_bkrp_BackupKey(struct dcesrv_call_state *dce_call,
 		return WERR_NOT_SUPPORTED;
 	}
 
-	ldb_ctx = samdb_connect(mem_ctx,
-				dce_call->event_ctx,
-				dce_call->conn->dce_ctx->lp_ctx,
-				system_session(dce_call->conn->dce_ctx->lp_ctx),
-				dce_call->conn->remote_address,
-				0);
-
+	ldb_ctx = connect_as_system(mem_ctx, dce_call);
 	if (samdb_rodc(ldb_ctx, &is_rodc) != LDB_SUCCESS) {
 		talloc_unlink(mem_ctx, ldb_ctx);
 		return WERR_INVALID_PARAMETER;
diff --git a/source4/rpc_server/wscript_build b/source4/rpc_server/wscript_build
index 31a5696..8e05eb8 100644
--- a/source4/rpc_server/wscript_build
+++ b/source4/rpc_server/wscript_build
@@ -133,7 +133,7 @@ else:
 		autoproto='backupkey/proto.h',
 		subsystem='dcerpc_server',
 		init_function='dcerpc_server_backupkey_init',
-		deps='samdb DCERPC_COMMON NDR_BACKUPKEY RPC_NDR_BACKUPKEY krb5 hx509 hcrypto gnutls gcrypt',
+		deps='samdb DCERPC_COMMON NDR_BACKUPKEY RPC_NDR_BACKUPKEY krb5 hx509 hcrypto gnutls gcrypt DCERPC_COMMON',
 		)
 
 
-- 
2.7.4


From 194f0407ecb56da85ff70667d147f6eb1991330e Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Wed, 4 Apr 2018 11:59:41 +1200
Subject: [PATCH 8/9] dsdb: audit samdb and password changes

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 at catalyst.net.nz>
---
 python/samba/tests/audit_log_base.py               |  150 ++
 python/samba/tests/audit_log_dsdb.py               |  594 ++++++
 python/samba/tests/audit_log_pass_change.py        |  335 +++
 selftest/target/Samba4.pm                          |    4 +
 source4/dsdb/samdb/ldb_modules/audit_log.c         | 1554 ++++++++++++++
 source4/dsdb/samdb/ldb_modules/audit_util.c        |  602 ++++++
 source4/dsdb/samdb/ldb_modules/samba_dsdb.c        |    3 +-
 .../dsdb/samdb/ldb_modules/tests/test_audit_log.c  | 2248 ++++++++++++++++++++
 .../dsdb/samdb/ldb_modules/tests/test_audit_util.c | 1260 +++++++++++
 source4/dsdb/samdb/ldb_modules/wscript_build       |   28 +-
 .../dsdb/samdb/ldb_modules/wscript_build_server    |   16 +
 source4/selftest/tests.py                          |   12 +
 12 files changed, 6803 insertions(+), 3 deletions(-)
 create mode 100644 python/samba/tests/audit_log_base.py
 create mode 100644 python/samba/tests/audit_log_dsdb.py
 create mode 100644 python/samba/tests/audit_log_pass_change.py
 create mode 100644 source4/dsdb/samdb/ldb_modules/audit_log.c
 create mode 100644 source4/dsdb/samdb/ldb_modules/audit_util.c
 create mode 100644 source4/dsdb/samdb/ldb_modules/tests/test_audit_log.c
 create mode 100644 source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c

diff --git a/python/samba/tests/audit_log_base.py b/python/samba/tests/audit_log_base.py
new file mode 100644
index 0000000..4f76c56
--- /dev/null
+++ b/python/samba/tests/audit_log_base.py
@@ -0,0 +1,150 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett <abartlet at 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
+
+
+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)
+
+        def isRemote(message):
+            remote = None
+            if message["type"] == "passwordChange":
+                remote = message["passwordChange"]["remoteAddress"]
+            elif message["type"] == "dsdbChange":
+                remote = message["dsdbChange"]["remoteAddress"]
+            elif message["type"] == "groupChange":
+                remote = message["groupChange"]["remoteAddress"]
+            elif message["type"] == "Authorization":
+                remote = message["Authorization"]["remoteAddress"]
+            else:
+                return False
+
+            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 waitForMessages(self, number, connection=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 len(self.context["messages"]) < number:
+            self.msg_ctx.loop_once(0.1)
+            if time.time() - start_time > 1:
+                self.connection = None
+                print("Timed out")
+                return []
+
+        self.connection = None
+        return self.context["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
index 0000000..41ed8b7
--- /dev/null
+++ b/python/samba/tests/audit_log_dsdb.py
@@ -0,0 +1,594 @@
+# Tests for SamDb password change audit logging.
+# Copyright (C) Andrew Bartlett <abartlet at 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
+        })
+
+    def tearDown(self):
+        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):
+
+        #
+        # Discard the messages from the Adding of a user in the setup
+        # code
+        messages = self.waitForMessages(6)
+        self.discardMessages()
+
+        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)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        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):
+
+        #
+        # Discard the messages from the Adding of a user in the setup
+        # code
+        messages = self.waitForMessages(5)
+        self.discardMessages()
+
+        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)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        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):
+
+        #
+        # Discard the messages from the setup code
+        messages = self.waitForMessages(5)
+        self.discardMessages()
+
+        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):
+
+        #
+        # Discard the messages from the setup code
+        messages = self.waitForMessages(5)
+        self.discardMessages()
+
+        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" +
+            "replace: 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.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.
+        messages = self.waitForMessages(5)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(5,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        audit = messages[4]["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):
+
+        #
+        # Discard the messages from the Adding of a user in the setup
+        # code
+        messages = self.waitForMessages(5)
+        self.discardMessages()
+
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        self.ldb.deleteuser(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]["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_secret_over_lsa(self):
+
+        #
+        # Discard the messages from the Adding of a user in the setup
+        # code
+        messages = self.waitForMessages(5)
+        self.discardMessages()
+
+        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)
+        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("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"])
+
+    def test_modify(self):
+
+        #
+        # Discard the messages from the setup code
+        messages = self.waitForMessages(5)
+        self.discardMessages()
+
+        #
+        # Add an attribute value
+        #
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        self.ldb.modify_ldif(
+            "dn: " + dn + "\n" +
+            "changetype: modify\n" +
+            "add: carLicense\n" +
+            "carLicense: license-01\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["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)
+        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)
+        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)
+        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)
+        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
index 0000000..2f7bfd7
--- /dev/null
+++ b/python/samba/tests/audit_log_pass_change.py
@@ -0,0 +1,335 @@
+# Tests for SamDb password change audit logging.
+# Copyright (C) Andrew Bartlett <abartlet at 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
+        })
+
+    def tearDown(self):
+        super(AuditLogPassChangeTests, self).tearDown()
+
+    def test_net_change_password(self):
+
+        #
+        # Discard messages from the user creation in setup.
+        #
+        messages = self.waitForMessages(1)
+        self.discardMessages()
+
+        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)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        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):
+
+        #
+        # Discard messages from the user creation in setup.
+        #
+        messages = self.waitForMessages(1)
+        self.discardMessages()
+
+        self.ldb.newuser(SECOND_USER_NAME, SECOND_USER_PASS)
+
+        #
+        # Get the password reset from the user add
+        #
+        messages = self.waitForMessages(1)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        dn = "CN=" + SECOND_USER_NAME + ",CN=Users," + self.base_dn
+        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
+
+        messages = self.waitForMessages(1, net)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        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):
+
+        #
+        # Discard messages from the user creation in setup.
+        #
+        messages = self.waitForMessages(1)
+        self.discardMessages()
+
+        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)
+        print("Received %d messages" % len(messages))
+        self.assertEquals(1,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+        dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
+        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):
+
+        #
+        # Discard messages from the user creation in setup.
+        #
+        messages = self.waitForMessages(1)
+        self.discardMessages()
+
+        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]["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):
+
+        #
+        # Discard messages from the user creation in setup.
+        #
+        messages = self.waitForMessages(1)
+        self.discardMessages()
+
+        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" +
+            "replace: 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]["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.
+        messages = self.waitForMessages(1)
+        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.
+        #
+        dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
+        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"]))
diff --git a/selftest/target/Samba4.pm b/selftest/target/Samba4.pm
index 14e312f..3df226f 100755
--- a/selftest/target/Samba4.pm
+++ b/selftest/target/Samba4.pm
@@ -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
index 0000000..40d9703
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/audit_log.c
@@ -0,0 +1,1554 @@
+/*
+   ldb database library
+
+   Copyright (C) Andrew Bartlett <abartlet at 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 (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 = get_remote_address(ldb);
+	if (remote != NULL && is_system_session(module)) {
+		as_system = true;
+		sid = get_actual_sid(ldb);
+		unique_session_token = get_actual_unique_session_token(ldb);
+	} else {
+		sid = get_user_sid(module);
+		unique_session_token = get_unique_session_token(module);
+	}
+	dn = get_primary_dn(request);
+	operation = 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 = get_message(request);
+	if (message != NULL) {
+		struct json_object attributes = 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 = get_remote_address(ldb);
+	sid = get_user_sid(module);
+	dn = get_primary_dn(request);
+	action = get_password_action(request, reply);
+	unique_session_token = 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 = get_remote_host(ldb, ctx);
+	sid = get_user_sid(module);
+	user_sid = dom_sid_string(ctx, sid);
+	timestamp = audit_get_timestamp(ctx);
+	action = get_password_action(request, reply);
+	dn = 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 = 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 (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 = get_remote_host(ldb, ctx);
+	if (remote_host != NULL && is_system_session(module)) {
+		sid = get_actual_sid(ldb);
+	} else {
+		sid = get_user_sid(module);
+	}
+	user_sid = dom_sid_string(ctx, sid);
+	timestamp = audit_get_timestamp(ctx);
+	op_name = get_operation_name(request);
+	dn = get_primary_dn(request);
+	new_dn = get_secondary_dn(request);
+
+	message = 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 = 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_hr(
+			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_hr(
+				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_password_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_hr(
+			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_hr(
+			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 = 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_hr(
+			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
index 0000000..71802e5
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/audit_util.c
@@ -0,0 +1,602 @@
+/*
+   ldb database module utility library
+
+   Copyright (C) Andrew Bartlett <abartlet at 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 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 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 *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 *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 *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 *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 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 *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 *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 *get_remote_host(
+	struct ldb_context *ldb,
+	TALLOC_CTX *mem_ctx)
+{
+	const struct tsocket_address *remote_address;
+	char* remote_host = NULL;
+
+	remote_address = 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* 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 *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 *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 *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 *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 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 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 = 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 (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++) {
+				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;
+}
diff --git a/source4/dsdb/samdb/ldb_modules/samba_dsdb.c b/source4/dsdb/samdb/ldb_modules/samba_dsdb.c
index 54ec6a2..baa30f9 100644
--- a/source4/dsdb/samdb/ldb_modules/samba_dsdb.c
+++ b/source4/dsdb/samdb/ldb_modules/samba_dsdb.c
@@ -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
index 0000000..3cde7ae
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/tests/test_audit_log.c
@@ -0,0 +1,2248 @@
+/*
+   Unit tests for the dsdb audit logging code code in audit_log.c
+
+   Copyright (C) Andrew Bartlett <abartlet at 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);
+}
+
+#ifdef HAVE_JANSSON
+/*
+ * 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);
+
+}
+
+#endif
+
+/*
+ * 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[] = {
+#ifdef HAVE_JANSSON
+		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),
+#endif
+		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
index 0000000..4f27a7c
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c
@@ -0,0 +1,1260 @@
+/*
+   Unit tests for the dsdb audit logging utility code code in audit_util.c
+
+   Copyright (C) Andrew Bartlett <abartlet at 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"
+
+#ifdef HAVE_JANSSON
+static void test_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));
+	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;
+	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;
+	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");
+	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");
+	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");
+	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!@#$%^&*()");
+
+	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!@#$%^&*()");
+
+	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;
+	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;
+	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_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 = 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 = 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 = 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 = 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 = 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 = 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 = 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);
+}
+#endif
+
+static void test_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 = get_remote_address(ldb);
+	assert_null(ts);
+
+	/*
+	 * opaque set to null, should return NULL
+	 */
+	ldb_set_opaque(ldb, "remoteAddress", NULL);
+	ts = 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 = get_remote_address(ldb);
+	assert_non_null(ts);
+	assert_ptr_equal(in, ts);
+
+	TALLOC_FREE(ldb);
+	TALLOC_FREE(ctx);
+
+}
+
+static void test_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 = 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 = 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_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 = get_user_sid(module);
+	assert_null(sid);
+
+	/*
+	 * Now add a NULL session info
+	 */
+	ldb_set_opaque(ldb, "sessionInfo", NULL);
+	sid = 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 = 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 = 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 = 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 = 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 = get_user_sid(module);
+	assert_null(sid);
+
+	TALLOC_FREE(ctx);
+}
+
+static void test_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 = get_actual_sid(ldb);
+	assert_null(sid);
+
+	/*
+	 * Now add a NULL session info
+	 */
+	ldb_set_opaque(ldb, "networkSessionInfo", NULL);
+	sid = 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 = 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 = 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 = 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 = 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 = get_actual_sid(ldb);
+	assert_null(sid);
+
+	TALLOC_FREE(ctx);
+}
+
+static void test_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(is_system_session(module));
+
+	/*
+	 * Now add a NULL session info
+	 */
+	ldb_set_opaque(ldb, "sessionInfo", NULL);
+	assert_false(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(is_system_session(module));
+
+	/*
+	 * Now add an empty security token.
+	 */
+	token = talloc_zero(ctx, struct security_token);
+	sess->security_token = token;
+	assert_false(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(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(is_system_session(module));
+
+	/*
+	 * Add a single SID, system sid
+	 */
+	token->num_sids = 1;
+	sids[0] = global_sid_System;
+	token->sids = sids;
+	assert_true(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(is_system_session(module));
+
+	/*
+	 * Now test a null sid in the first position
+	 */
+	token->num_sids = 1;
+	token->sids = NULL;
+	sid = get_user_sid(module);
+	assert_null(sid);
+
+	TALLOC_FREE(ctx);
+}
+
+static void test_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 = get_unique_session_token(module);
+	assert_null(guid);
+
+	/*
+	 * Now add a NULL session info
+	 */
+	ldb_set_opaque(ldb, "sessionInfo", NULL);
+	guid = 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 = 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 = 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_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 = get_actual_unique_session_token(ldb);
+	assert_null(guid);
+
+	/*
+	 * Now add a NULL session info
+	 */
+	ldb_set_opaque(ldb, "networkSessionInfo", NULL);
+	guid = 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 = 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 = 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_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 = 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 = 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 = get_remote_host(ldb, ctx);
+	assert_string_equal("ipv4:127.0.0.1:42", rh);
+	TALLOC_FREE(rh);
+
+	TALLOC_FREE(ctx);
+
+}
+
+static void test_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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = get_primary_dn(req);
+	assert_null(s);
+
+	/*
+	 * Try a delete with a dn
+	 */
+	req->operation = LDB_DELETE;
+	req->op.del.dn = dn;
+	s = 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 = get_primary_dn(req);
+	assert_null(s);
+
+	/*
+	 * Try a rename with a dn
+	 */
+	req->operation = LDB_RENAME;
+	req->op.rename.olddn = dn;
+	s = 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 = get_primary_dn(req);
+	assert_null(s);
+
+	TALLOC_FREE(ctx);
+}
+
+static void test_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 = get_message(req);
+	assert_null(r);
+
+	/*
+	 * Test an add message
+	 */
+	req->operation = LDB_ADD;
+	req->op.add.message = msg;
+	r = get_message(req);
+	assert_ptr_equal(msg, r);
+
+	/*
+	 * Test a modify message
+	 */
+	req->operation = LDB_MODIFY;
+	req->op.mod.message = msg;
+	r = get_message(req);
+	assert_ptr_equal(msg, r);
+
+	/*
+	 * Test a Delete message, i.e. trigger the default case
+	 */
+	req->operation = LDB_DELETE;
+	r = get_message(req);
+	assert_null(r);
+
+	TALLOC_FREE(ctx);
+}
+
+static void test_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 = get_secondary_dn(req);
+	assert_null(s);
+
+	/*
+	 * Try a rename without a dn
+	 */
+	req->operation = LDB_RENAME;
+	req->op.rename.newdn = NULL;
+	s = get_secondary_dn(req);
+	assert_null(s);
+
+	/*
+	 * Try a rename with a dn
+	 */
+	req->operation = LDB_RENAME;
+	req->op.rename.newdn = dn;
+	s = 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 = get_primary_dn(req);
+	assert_null(s);
+
+	TALLOC_FREE(ctx);
+}
+
+static void test_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", get_operation_name(req));
+
+	req->operation =  LDB_ADD;
+	assert_string_equal("Add", get_operation_name(req));
+
+	req->operation =  LDB_MODIFY;
+	assert_string_equal("Modify", get_operation_name(req));
+
+	req->operation =  LDB_DELETE;
+	assert_string_equal("Delete", get_operation_name(req));
+
+	req->operation =  LDB_RENAME;
+	assert_string_equal("Rename", get_operation_name(req));
+
+	req->operation =  LDB_EXTENDED;
+	assert_string_equal("Extended", get_operation_name(req));
+
+	req->operation =  LDB_REQ_REGISTER_CONTROL;
+	assert_string_equal("Register Control", get_operation_name(req));
+
+	req->operation =  LDB_REQ_REGISTER_PARTITION;
+	assert_string_equal("Register Partition", get_operation_name(req));
+
+	/*
+	 * Trigger the default case
+	 */
+	req->operation =  -1;
+	assert_string_equal("Unknown", get_operation_name(req));
+
+	TALLOC_FREE(ctx);
+}
+
+static void test_get_modification_action(void **state)
+{
+	assert_string_equal(
+		"add",
+		get_modification_action(LDB_FLAG_MOD_ADD));
+	assert_string_equal(
+		"delete",
+		get_modification_action(LDB_FLAG_MOD_DELETE));
+	assert_string_equal(
+		"replace",
+		get_modification_action(LDB_FLAG_MOD_REPLACE));
+	/*
+	 * Trigger the default case
+	 */
+	assert_string_equal(
+		"unknown",
+		get_modification_action(0));
+}
+
+static void test_is_password_attribute(void **state)
+{
+	assert_true(is_password_attribute("userPassword"));
+	assert_true(is_password_attribute("clearTextPassword"));
+	assert_true(is_password_attribute("unicodePwd"));
+	assert_true(is_password_attribute("dBCSPwd"));
+
+	assert_false(is_password_attribute("xserPassword"));
+}
+
+static void test_redact_attribute(void **state)
+{
+	assert_true(redact_attribute("userPassword"));
+
+	assert_true(redact_attribute("pekList"));
+	assert_true(redact_attribute("clearTextPassword"));
+	assert_true(redact_attribute("initialAuthIncoming"));
+
+	assert_false(redact_attribute("supaskrt"));
+}
+
+int main(void) {
+	const struct CMUnitTest tests[] = {
+#ifdef HAVE_JANSSON
+		cmocka_unit_test(test_add_ldb_value),
+		cmocka_unit_test(test_attributes_json),
+#endif
+		cmocka_unit_test(test_get_remote_address),
+		cmocka_unit_test(test_get_ldb_error_string),
+		cmocka_unit_test(test_get_user_sid),
+		cmocka_unit_test(test_get_actual_sid),
+		cmocka_unit_test(test_is_system_session),
+		cmocka_unit_test(test_get_unique_session_token),
+		cmocka_unit_test(test_get_actual_unique_session_token),
+		cmocka_unit_test(test_get_remote_host),
+		cmocka_unit_test(test_get_primary_dn),
+		cmocka_unit_test(test_get_message),
+		cmocka_unit_test(test_get_secondary_dn),
+		cmocka_unit_test(test_get_operation_name),
+		cmocka_unit_test(test_get_modification_action),
+		cmocka_unit_test(test_is_password_attribute),
+		cmocka_unit_test(test_redact_attribute),
+	};
+
+	cmocka_set_message_output(CM_OUTPUT_SUBUNIT);
+	return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/source4/dsdb/samdb/ldb_modules/wscript_build b/source4/dsdb/samdb/ldb_modules/wscript_build
index 9e0ac28..da21e96 100644
--- a/source4/dsdb/samdb/ldb_modules/wscript_build
+++ b/source4/dsdb/samdb/ldb_modules/wscript_build
@@ -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,30 @@ bld.SAMBA_BINARY('test_encrypted_secrets',
             DSDB_MODULE_HELPERS
         ''',
         install=False)
+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")
diff --git a/source4/dsdb/samdb/ldb_modules/wscript_build_server b/source4/dsdb/samdb/ldb_modules/wscript_build_server
index 368260a..6c821fb 100644
--- a/source4/dsdb/samdb/ldb_modules/wscript_build_server
+++ b/source4/dsdb/samdb/ldb_modules/wscript_build_server
@@ -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
+        '''
+	)
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 5359316..c317975 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -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",
@@ -1069,3 +1077,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")])
-- 
2.7.4


From 9fc2f059e2803869af0a9067a8542c10276bc897 Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Mon, 16 Apr 2018 14:03:14 +1200
Subject: [PATCH 9/9] dsdb: Audit group membership changes

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 at catalyst.net.nz>
---
 python/samba/tests/group_audit.py                  |  355 +++++
 selftest/target/Samba4.pm                          |    2 +
 source4/dsdb/samdb/ldb_modules/group_audit.c       | 1362 ++++++++++++++++++++
 source4/dsdb/samdb/ldb_modules/samba_dsdb.c        |    1 +
 .../samdb/ldb_modules/tests/test_group_audit.c     |  736 +++++++++++
 .../ldb_modules/tests/test_group_audit.valgrind    |   19 +
 source4/dsdb/samdb/ldb_modules/wscript_build       |   12 +
 .../dsdb/samdb/ldb_modules/wscript_build_server    |   16 +
 source4/selftest/tests.py                          |    4 +
 9 files changed, 2507 insertions(+)
 create mode 100644 python/samba/tests/group_audit.py
 create mode 100644 source4/dsdb/samdb/ldb_modules/group_audit.c
 create mode 100644 source4/dsdb/samdb/ldb_modules/tests/test_group_audit.c
 create mode 100644 source4/dsdb/samdb/ldb_modules/tests/test_group_audit.valgrind

diff --git a/python/samba/tests/group_audit.py b/python/samba/tests/group_audit.py
new file mode 100644
index 0000000..53a8bf6
--- /dev/null
+++ b/python/samba/tests/group_audit.py
@@ -0,0 +1,355 @@
+# Tests for SamDb password change audit logging.
+# Copyright (C) Andrew Bartlett <abartlet at 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")
diff --git a/selftest/target/Samba4.pm b/selftest/target/Samba4.pm
index 3df226f..7abc16e 100755
--- a/selftest/target/Samba4.pm
+++ b/selftest/target/Samba4.pm
@@ -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
index 0000000..415b669
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/group_audit.c
@@ -0,0 +1,1362 @@
+/*
+   ldb database library
+
+   Copyright (C) Andrew Bartlett <abartlet at 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 = get_remote_address(ldb);
+	sid = get_user_sid(module);
+	unique_session_token = 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 = get_remote_host(ldb, ctx);
+	sid = 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 = 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_hr(
+			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 = 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_hr(
+			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 = 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 = 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 = 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 = 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 = 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);
+}
diff --git a/source4/dsdb/samdb/ldb_modules/samba_dsdb.c b/source4/dsdb/samdb/ldb_modules/samba_dsdb.c
index baa30f9..fa58f19 100644
--- a/source4/dsdb/samdb/ldb_modules/samba_dsdb.c
+++ b/source4/dsdb/samdb/ldb_modules/samba_dsdb.c
@@ -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
index 0000000..3d451a5
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.c
@@ -0,0 +1,736 @@
+/*
+   Unit tests for the dsdb group auditing code in group_audit.c
+
+   Copyright (C) Andrew Bartlett <abartlet at 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_hr(
+	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);
+}
+
+#ifdef HAVE_JANSSON
+/*
+ * 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();
+}
+#endif /* #ifdef HAVE_JANSSON */
+
+/*
+ * 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[] = {
+#ifdef HAVE_JANSSON
+		cmocka_unit_test(test_audit_group_json),
+		cmocka_unit_test(test_place_holder),
+#endif
+		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
index 0000000..1cf2b4e
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.valgrind
@@ -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
+}
+
+
diff --git a/source4/dsdb/samdb/ldb_modules/wscript_build b/source4/dsdb/samdb/ldb_modules/wscript_build
index da21e96..93c3563 100644
--- a/source4/dsdb/samdb/ldb_modules/wscript_build
+++ b/source4/dsdb/samdb/ldb_modules/wscript_build
@@ -64,6 +64,18 @@ bld.SAMBA_BINARY('test_audit_log',
             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")
diff --git a/source4/dsdb/samdb/ldb_modules/wscript_build_server b/source4/dsdb/samdb/ldb_modules/wscript_build_server
index 6c821fb..e5c5032 100644
--- a/source4/dsdb/samdb/ldb_modules/wscript_build_server
+++ b/source4/dsdb/samdb/ldb_modules/wscript_build_server
@@ -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
+        '''
+	)
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index c317975..c7f4105 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -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",
-- 
2.7.4

-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 473 bytes
Desc: OpenPGP digital signature
URL: <http://lists.samba.org/pipermail/samba-technical/attachments/20180531/1d21d1be/signature-0001.sig>


More information about the samba-technical mailing list