[WIP] Log database changes.

Jeremy Allison jra at samba.org
Wed May 30 23:18:13 UTC 2018


On Wed, May 30, 2018 at 03:28:07PM +1200, Gary Lockyer via samba-technical wrote:
> Attached is the current state of this work.
> 
> I plan to clean up the commits and fix any auto build issues tomorrow
> ready for final review and push.
> 
> Any comments greatly appreciated

FYI, I *still* hate the void returns on functions that
can fail....

There will be a reconing coming :-) :-).




> From 00f5bf8e61ffe3b652df3d155145ba746cc432c5 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 01/10] fix up audit_logging
> 
> 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 8c1ea246efa93d0c86232eb1950e95faaf6007aa 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 02/10] auth tests: Deregister the event listener
> 
> 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 b06fca729ddeba5ae49c5ef8b2abd77b013ad2e5 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 03/10] cldap: set the remote ip address
> 
> 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 1bfc747118903dafe71a610bc10376d298d89951 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 04/10] dsdb acl: Copy dsdb_control_password_acl_validation
>  into reply
> 
> 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 5a989963d44753ff10d82094f4cd36272801df5d Mon Sep 17 00:00:00 2001
> From: Gary Lockyer <gary at catalyst.net.nz>
> Date: Mon, 21 May 2018 14:31:57 +1200
> Subject: [PATCH 05/10] dsdb partition.c: Make partition_copy_all aysnc.
> 
> partition_copy_all uses ldb_wait to wait for the update to the primary
> partition to complete, when updating a special dn.  If a module higher
> up the chain inserts a callback, the code blocks in ldb_wait and does
> not complete.  This change replaces the ldb_wait logic with a callback.
> 
> Currently there is no code that triggers this bug, however the up coming
> audit logging changes do trigger this bug.
> 
> Signed-off-by: Gary Lockyer <gary at catalyst.net.nz>
> ---
>  source4/dsdb/samdb/ldb_modules/partition.c | 200 ++++++++++++++++++++++++++---
>  1 file changed, 179 insertions(+), 21 deletions(-)
> 
> diff --git a/source4/dsdb/samdb/ldb_modules/partition.c b/source4/dsdb/samdb/ldb_modules/partition.c
> index 9fb1b9d..0068571 100644
> --- a/source4/dsdb/samdb/ldb_modules/partition.c
> +++ b/source4/dsdb/samdb/ldb_modules/partition.c
> @@ -394,32 +394,37 @@ static int partition_send_all(struct ldb_module *module,
>  	return partition_call_first(ac);
>  }
>  
> +struct partition_copy_context {
> +	struct ldb_module *module;
> +	struct partition_context *partition_context;
> +	struct ldb_request *request;
> +	struct ldb_dn *dn;
> +};
>  
> -/**
> - * send an operation to the top partition, then copy the resulting
> - * object to all other partitions
> +/*
> + * A special DN has been updated in the primary partition. Now propagate those
> + * changes to the remaining partitions.
> + *
> + * Note: that the operations are asyncchonous and this fuction is called
> + *       from partition_copy_all_callback_handler in response to an async
> + *       callback.
>   */
> -static int partition_copy_all(struct ldb_module *module,
> -			      struct partition_context *ac,
> -			      struct ldb_request *req,
> -			      struct ldb_dn *dn)
> +static int partition_copy_all_callback_action(
> +	struct ldb_module *module,
> +	struct partition_context *ac,
> +	struct ldb_request *req,
> +	struct ldb_dn *dn,
> +	int ret)
> +
>  {
> +
>  	unsigned int i;
> -	struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module),
> -							      struct partition_private_data);
> -	int ret, search_ret;
> +	struct partition_private_data *data =
> +		talloc_get_type(
> +			ldb_module_get_private(module),
> +			struct partition_private_data);
> +	int search_ret;
>  	struct ldb_result *res;
> -
> -	/* do the request on the top level sam.ldb synchronously */
> -	ret = ldb_next_request(module, req);
> -	if (ret != LDB_SUCCESS) {
> -		return ret;
> -	}
> -	ret = ldb_wait(req->handle, LDB_WAIT_ALL);
> -	if (ret != LDB_SUCCESS) {
> -		return ret;
> -	}
> -
>  	/* now fetch the resulting object, and then copy it to all the
>  	 * other partitions. We need this approach to cope with the
>  	 * partitions getting out of sync. If for example the
> @@ -521,6 +526,159 @@ static int partition_copy_all(struct ldb_module *module,
>  	return ldb_module_done(req, NULL, NULL, LDB_SUCCESS);
>  }
>  
> +
> +/*
> + * @brief call back function for the ldb operations on special DN's.
> + *
> + * As the LDB operations are async, and we wish to use the result
> + * 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 partition_copy_all_callback_handler(
> +	struct ldb_request *req,
> +	struct ldb_reply *ares)
> +{
> +	struct partition_copy_context *ac = NULL;
> +	int error = ares->error;
> +
> +	ac = talloc_get_type(
> +		req->context,
> +		struct partition_copy_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:
> +		error = partition_copy_all_callback_action(
> +			ac->module,
> +			ac->partition_context,
> +			ac->request,
> +			ac->dn,
> +			ares->error);
> +		return ldb_module_done(
> +			ac->request,
> +			ares->controls,
> +			ares->response,
> +			error);
> +
> +	default:
> +		/* Can't happen */
> +		return LDB_ERR_OPERATIONS_ERROR;
> +	}
> +}
> +
> +/**
> + * send an operation to the top partition, then copy the resulting
> + * object to all other partitions.
> + */
> +static int partition_copy_all(
> +	struct ldb_module *module,
> +	struct partition_context *partition_context,
> +	struct ldb_request *req,
> +	struct ldb_dn *dn)
> +{
> +	struct ldb_request *new_req = NULL;
> +	struct ldb_context *ldb = NULL;
> +	struct partition_copy_context *context = NULL;
> +
> +	int ret;
> +
> +	ldb = ldb_module_get_ctx(module);
> +
> +	context = talloc_zero(req, struct partition_copy_context);
> +	if (context == NULL) {
> +		return ldb_oom(ldb);
> +	}
> +	context->module = module;
> +	context->request = req;
> +	context->dn = dn;
> +	context->partition_context = partition_context;
> +
> +	switch (req->operation) {
> +	case LDB_ADD:
> +		ret = ldb_build_add_req(
> +			&new_req,
> +			ldb,
> +			req,
> +			req->op.add.message,
> +			req->controls,
> +			context,
> +			partition_copy_all_callback_handler,
> +			req);
> +		break;
> +	case LDB_MODIFY:
> +		ret = ldb_build_mod_req(
> +			&new_req,
> +			ldb,
> +			req,
> +			req->op.mod.message,
> +			req->controls,
> +			context,
> +			partition_copy_all_callback_handler,
> +			req);
> +		break;
> +	case LDB_DELETE:
> +		ret = ldb_build_del_req(
> +			&new_req,
> +			ldb,
> +			req,
> +			req->op.del.dn,
> +			req->controls,
> +			context,
> +			partition_copy_all_callback_handler,
> +			req);
> +		break;
> +	case LDB_RENAME:
> +		ret = ldb_build_rename_req(
> +			&new_req,
> +			ldb,
> +			req,
> +			req->op.rename.olddn,
> +			req->op.rename.newdn,
> +			req->controls,
> +			context,
> +			partition_copy_all_callback_handler,
> +			req);
> +		break;
> +	default:
> +		/*
> +		 * Shouldn't happen.
> +		 */
> +		ldb_debug(
> +			ldb,
> +			LDB_DEBUG_ERROR,
> +			"Unexpected opertation type (%d)\n", req->operation);
> +		ret = LDB_ERR_OPERATIONS_ERROR;
> +		break;
> +	}
> +	if (ret != LDB_SUCCESS) {
> +		return ret;
> +	}
> +	return ldb_next_request(module, new_req);
> +}
>  /**
>   * Figure out which backend a request needs to be aimed at.  Some
>   * requests must be replicated to all backends
> -- 
> 2.7.4
> 
> 
> From 475028c89a49fe51c9c513f03d26b4c238a1eff0 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 06/10] rpc_server: common routine to open ldb in 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 902fbf1dff2a4948d6116c08486cc0c0b19576e7 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 07/10] rpc_server lsa: pass remote connection data
> 
> 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 45cd19604784387ed8121845ee7527f56a4bc076 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 08/10] rpc_server backupkey:w: pass remote connection data
> 
> 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 | 8 +-------
>  2 files changed, 3 insertions(+), 13 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..cd192aa 100644
> --- a/source4/rpc_server/backupkey/dcesrv_backupkey_heimdal.c
> +++ b/source4/rpc_server/backupkey/dcesrv_backupkey_heimdal.c
> @@ -1814,13 +1814,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;
> -- 
> 2.7.4
> 
> 
> From 9891a095538df1e211829688442c04909319f901 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 09/10] dsdb: audit samdb and password changes
> 
> 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 def1dfe01434f987ca1af55b60319efe183dcf55 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 10/10] SamDb: Audit group membership changes.:w
> 
> 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
> 







More information about the samba-technical mailing list