/*
   ldb database library

   Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/*
 * Provide an audit log of changes made to group memberships
 *
 */

#include "includes.h"
#include "ldb_module.h"
#include "lib/audit_logging/audit_logging.h"
#include "librpc/gen_ndr/windows_event_ids.h"

#include "dsdb/samdb/samdb.h"
#include "dsdb/samdb/ldb_modules/util.h"
#include "dsdb/samdb/ldb_modules/audit_util_proto.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 1
#define GROUP_LOG_LVL 5

static const char *const group_attrs[] = {"member", "groupType", NULL};
static const char *const group_type_attr[] = {"groupType", 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;
}

/*
 * @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.
 * 	   NULL if an error was detected
 */
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 enum event_id_type event_id,
					   const int status)
{
	struct ldb_context *ldb = NULL;
	const struct dom_sid *sid = NULL;
	struct json_object wrapper = json_empty_object;
	struct json_object audit = json_empty_object;
	const struct tsocket_address *remote = NULL;
	const struct GUID *unique_session_token = NULL;
	struct GUID *transaction_id = NULL;
	int rc = 0;

	ldb = ldb_module_get_ctx(discard_const(module));

	remote = dsdb_audit_get_remote_address(ldb);
	sid = dsdb_audit_get_user_sid(module);
	unique_session_token = dsdb_audit_get_unique_session_token(module);
	transaction_id = get_transaction_id(request);

	audit = json_new_object();
	if (json_is_invalid(&audit)) {
		goto failure;
	}
	rc = json_add_version(&audit, AUDIT_MAJOR, AUDIT_MINOR);
	if (rc != 0) {
		goto failure;
	}
	if (event_id != EVT_ID_NONE) {
		rc = json_add_int(&audit, "eventId", event_id);
		if (rc != 0) {
			goto failure;
		}
	}
	rc = json_add_int(&audit, "statusCode", status);
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_string(&audit, "status", ldb_strerror(status));
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_string(&audit, "action", action);
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_address(&audit, "remoteAddress", remote);
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_sid(&audit, "userSid", sid);
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_string(&audit, "group", group);
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_guid(&audit, "transactionId", transaction_id);
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_guid(&audit, "sessionId", unique_session_token);
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_string(&audit, "user", user);
	if (rc != 0) {
		goto failure;
	}

	wrapper = json_new_object();
	if (json_is_invalid(&wrapper)) {
		goto failure;
	}
	rc = json_add_timestamp(&wrapper);
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_string(&wrapper, "type", AUDIT_JSON_TYPE);
	if (rc != 0) {
		goto failure;
	}
	rc = json_add_object(&wrapper, AUDIT_JSON_TYPE, &audit);
	if (rc != 0) {
		goto failure;
	}

	return wrapper;
failure:
	/*
	 * On a failure audit will not have been added to wrapper so it
	 * needs to free it to avoid a leak.
	 *
	 * wrapper is freed to invalidate it as it will have only been
	 * partially constructed and may be inconsistent.
	 *
	 * All the json manipulation routines handle a freed object correctly
	 */
	json_free(&audit);
	json_free(&wrapper);
	DBG_ERR("Failed to create group change JSON log message\n");
	return wrapper;
}

/*
 * @brief generate a human readable log entry for a group change.
 *
 * Generate a human readable log entry containing details of a users group
 * change.
 *
 * @param ctx the talloc context owning the returned log entry
 * @param module the ldb module
 * @param request the ldb_request
 * @param action the change action being performed
 * @param user the user name
 * @param group the group name
 * @param status the ldb status code for the ldb operation.
 *
 * @return A human readable log line.
 */
static char *audit_group_human_readable(
	TALLOC_CTX *mem_ctx,
	const struct ldb_module *module,
	const struct ldb_request *request,
	const char *action,
	const char *user,
	const char *group,
	const int status)
{
	struct ldb_context *ldb = NULL;
	const char *remote_host = NULL;
	const struct dom_sid *sid = NULL;
	const char *user_sid = NULL;
	const char *timestamp = NULL;
	char *log_entry = NULL;

	TALLOC_CTX *ctx = talloc_new(NULL);

	ldb = ldb_module_get_ctx(discard_const(module));

	remote_host = dsdb_audit_get_remote_host(ldb, ctx);
	sid = dsdb_audit_get_user_sid(module);
	user_sid = dom_sid_string(ctx, sid);
	timestamp = audit_get_timestamp(ctx);

	log_entry = talloc_asprintf(
		mem_ctx,
		"[%s] at [%s] status [%s] "
		"Remote host [%s] SID [%s] Group [%s] User [%s]",
		action,
		timestamp,
		ldb_strerror(status),
		remote_host,
		user_sid,
		group,
		user);
	TALLOC_FREE(ctx);
	return log_entry;
}

/*
 * @brief generate an array of parsed_dns, deferring the actual parsing.
 *
 * Get an array of 'struct parsed_dns' without the parsing.
 * The parsed_dns are parsed only when needed to avoid the expense of parsing.
 *
 * This procedure assumes that the dn's are sorted in GUID order and contains
 * no duplicates.  This should be valid as the module sits below repl_meta_data
 * which ensures this.
 *
 * @param mem_ctx The memory context that will own the generated array
 * @param el The message element used to generate the array.
 *
 * @return an array of struct parsed_dns, or NULL in the event of an error
 */
static struct parsed_dn *get_parsed_dns(
	TALLOC_CTX *mem_ctx,
	struct ldb_message_element *el)
{
	int ret;
	struct parsed_dn *pdn = NULL;

	if (el == NULL || el->num_values == 0) {
		return NULL;
	}

	ret = get_parsed_dns_trusted(mem_ctx, el, &pdn);
	if (ret == LDB_ERR_OPERATIONS_ERROR) {
		DBG_ERR("Out of memory\n");
		return NULL;
	}
	return pdn;

}

enum dn_compare_result {
	LESS_THAN,
	BINARY_EQUAL,
	EQUAL,
	GREATER_THAN
};
/*
 * @brief compare parsed_dn, using GUID ordering
 *
 * Compare two parsed_dn structures, using GUID ordering.
 * To avoid the overhead of parsing the DN's this function does a binary
 * compare first. The DN's are only parsed if they are not equal at a binary
 * level.
 *
 * @param ctx talloc context that will own the parsed dsdb_dn
 * @param ldb ldb_context
 * @param dn1 The first dn
 * @param dn2 The second dn
 *
 * @return BINARY_EQUAL values are equal at a binary level
 *         EQUAL        DN's are equal but the meta data is different
 *         LESS_THAN    dn1's GUID is less than dn2's GUID
 *         GREATER_THAN dn1's GUID is greater than  dn2's GUID
 *
 */
static enum dn_compare_result dn_compare(
	TALLOC_CTX *mem_ctx,
	struct ldb_context *ldb,
	struct parsed_dn *dn1,
	struct parsed_dn *dn2) {

	int res = 0;

	/*
	 * Do a binary compare first to avoid unnecessary parsing
	 */
	if (data_blob_cmp(dn1->v, dn2->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 (dn1->dsdb_dn == NULL) {
		really_parse_trusted_dn(
			mem_ctx,
			ldb,
			dn1,
			LDB_SYNTAX_DN);
	}
	if (dn2->dsdb_dn == NULL) {
		really_parse_trusted_dn(
			mem_ctx,
			ldb,
			dn2,
			LDB_SYNTAX_DN);
	}

	res = ndr_guid_compare(&dn1->guid, &dn2->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.
 * There is no windows event id associated with a Primary Group change.
 * However for a new user we generate an added to group event.
 *
 * @param module The ldb module.
 * @param request The request being logged.
 * @param action Description of the action being performed.
 * @param group The linearized for of the group DN
 * @param status the LDB status code for the processing of the request.
 *
 */
static void log_primary_group_change(
	struct ldb_module *module,
	const struct ldb_request *request,
	const char *action,
	const char *group,
	const int  status)
{
	const char *user = NULL;

	struct audit_context *ac =
		talloc_get_type(
			ldb_module_get_private(module),
			struct audit_context);

	TALLOC_CTX *ctx = talloc_new(NULL);

	user = dsdb_audit_get_primary_dn(request);
	if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL)) {
		char *message = NULL;
		message = audit_group_human_readable(
			ctx,
			module,
			request,
			action,
			user,
			group,
			status);
		audit_log_human_text(
			AUDIT_HR_TAG,
			message,
			DBGC_DSDB_GROUP_AUDIT,
			GROUP_LOG_LVL);
		TALLOC_FREE(message);
	}

	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, EVT_ID_NONE, status);
		audit_log_json(
			&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);
		if (request->operation == LDB_ADD) {
			/*
			 * Have just added a user, generate a groupChange
			 * message indicating the user has been added to their
			 * new PrimaryGroup.
			 */
		}
	}
	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 enum event_id_type event_id,
				  const int status)
{
	const char *group = NULL;
	struct audit_context *ac =
		talloc_get_type(
			ldb_module_get_private(module),
			struct audit_context);

	TALLOC_CTX *ctx = talloc_new(NULL);
	group = dsdb_audit_get_primary_dn(request);
	if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL)) {
		char *message = NULL;
		message = audit_group_human_readable(
			ctx,
			module,
			request,
			action,
			user,
			group,
			status);
		audit_log_human_text(
			AUDIT_HR_TAG,
			message,
			DBGC_DSDB_GROUP_AUDIT,
			GROUP_LOG_LVL);
		TALLOC_FREE(message);
	}

	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, event_id, status);
		audit_log_json(
			&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);
	}
	TALLOC_FREE(ctx);
}

/*
 * @brief Get the windows event type id for removing a user from a group type.
 *
 * @param group_type the type of the current group, see libds/common/flags.h
 *
 * @return the Windows Event Id
 *
 */
static enum event_id_type get_remove_member_event(uint32_t group_type)
{

	switch (group_type) {
	case GTYPE_SECURITY_BUILTIN_LOCAL_GROUP:
		return EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP;
	case GTYPE_SECURITY_GLOBAL_GROUP:
		return EVT_ID_USER_REMOVED_FROM_GLOBAL_SEC_GROUP;
	case GTYPE_SECURITY_DOMAIN_LOCAL_GROUP:
		return EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP;
	case GTYPE_SECURITY_UNIVERSAL_GROUP:
		return EVT_ID_USER_REMOVED_FROM_UNIVERSAL_SEC_GROUP;
	case GTYPE_DISTRIBUTION_GLOBAL_GROUP:
		return EVT_ID_USER_REMOVED_FROM_GLOBAL_GROUP;
	case GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP:
		return EVT_ID_USER_REMOVED_FROM_LOCAL_GROUP;
	case GTYPE_DISTRIBUTION_UNIVERSAL_GROUP:
		return EVT_ID_USER_REMOVED_FROM_UNIVERSAL_GROUP;
	default:
		return EVT_ID_NONE;
	}
}

/*
 * @brief Get the windows event type id for adding a user to a group type.
 *
 * @param group_type the type of the current group, see libds/common/flags.h
 *
 * @return the Windows Event Id
 *
 */
static enum event_id_type get_add_member_event(uint32_t group_type)
{

	switch (group_type) {
	case GTYPE_SECURITY_BUILTIN_LOCAL_GROUP:
		return EVT_ID_USER_ADDED_TO_LOCAL_SEC_GROUP;
	case GTYPE_SECURITY_GLOBAL_GROUP:
		return EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP;
	case GTYPE_SECURITY_DOMAIN_LOCAL_GROUP:
		return EVT_ID_USER_ADDED_TO_LOCAL_SEC_GROUP;
	case GTYPE_SECURITY_UNIVERSAL_GROUP:
		return EVT_ID_USER_ADDED_TO_UNIVERSAL_SEC_GROUP;
	case GTYPE_DISTRIBUTION_GLOBAL_GROUP:
		return EVT_ID_USER_ADDED_TO_GLOBAL_GROUP;
	case GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP:
		return EVT_ID_USER_ADDED_TO_LOCAL_GROUP;
	case GTYPE_DISTRIBUTION_UNIVERSAL_GROUP:
		return EVT_ID_USER_ADDED_TO_UNIVERSAL_GROUP;
	default:
		return EVT_ID_NONE;
	}
}

/*
 * @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,
				   uint32_t group_type,
				   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;
			enum event_id_type event_id;
			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);
			event_id = get_remove_member_event(group_type);
			log_membership_change(
			    module, request, "Removed", user, event_id, 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;
				enum event_id_type event_id;
				user = ldb_dn_get_linearized(
					old_val->dsdb_dn->dn);
				event_id = get_remove_member_event(group_type);
				log_membership_change(module,
						      request,
						      "Removed",
						      user,
						      event_id,
						      status);
			} else {
				/*
				 * DN has been re-added
				 */
				const char *user = NULL;
				enum event_id_type event_id;
				user = ldb_dn_get_linearized(
					new_val->dsdb_dn->dn);
				event_id = get_add_member_event(group_type);
				log_membership_change(module,
						      request,
						      "Added",
						      user,
						      event_id,
						      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;
			enum event_id_type event_id;
			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);
			event_id = get_add_member_event(group_type);
			log_membership_change(
			    module, request, "Added", user, event_id, status);
			new_i++;
		}
	}

	TALLOC_FREE(ctx);
}

/*
 * @brief log a group change message for a newly added user.
 *
 * When a user is added we need to generate a GroupChange Add message to
 * log that the user has been added to their PrimaryGroup
 */
static void log_new_user_added_to_primary_group(
    TALLOC_CTX *ctx,
    struct audit_callback_context *acc,
    const char *group,
    const int status)
{
	uint32_t group_type;
	enum event_id_type event_id = EVT_ID_NONE;
	struct ldb_result *res = NULL;
	struct ldb_dn *group_dn = NULL;
	struct ldb_context *ldb = NULL;
	int ret;

	ldb = ldb_module_get_ctx(acc->module);
	group_dn = ldb_dn_new(ctx, ldb, group);
	ret = dsdb_module_search_dn(acc->module,
				    ctx,
				    &res,
				    group_dn,
				    group_type_attr,
				    DSDB_FLAG_NEXT_MODULE |
					DSDB_SEARCH_REVEAL_INTERNALS |
					DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT,
				    NULL);
	if (ret == LDB_SUCCESS) {
		const char *user = NULL;
		group_type =
		    ldb_msg_find_attr_as_uint(res->msgs[0], "groupType", 0);
		event_id = get_add_member_event(group_type);
		user = dsdb_audit_get_primary_dn(acc->request);
		log_membership_change(
		    acc->module, acc->request, "Added", user, event_id, status);
	}
}

/*
 * @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 = UINT32_MAX;
	struct dom_sid *account_sid = NULL;
	int ret;
	const struct ldb_message *msg = dsdb_audit_get_message(acc->request);

	if (status == LDB_SUCCESS && msg != NULL) {
		struct ldb_result *res = NULL;
		ret = dsdb_module_search_dn(
			acc->module,
			ctx,
			&res,
			msg->dn,
			primary_group_attr,
			DSDB_FLAG_NEXT_MODULE |
			DSDB_SEARCH_REVEAL_INTERNALS |
			DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT,
			NULL);
		if (ret == LDB_SUCCESS) {
			new_rid = ldb_msg_find_attr_as_uint(
				msg,
				"primaryGroupID",
				~0);
			account_sid = samdb_result_dom_sid(
				ctx,
				res->msgs[0],
				"objectSid");
		}
	}
	/*
	 * If we don't have a new value then the user has been deleted
	 * which we currently do not log.
	 * Otherwise only log if the primary group has actually changed.
	 */
	if (account_sid != NULL &&
	    new_rid != UINT32_MAX &&
	    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);
		/*
		 * Are we adding a new user with the primaryGroupID
		 * set. If so and we're generating JSON audit logs, will need to
		 * generate an "Add" message with the appropriate windows
		 * event id.
		 */
		if (acc->request->operation == LDB_ADD) {
			log_new_user_added_to_primary_group(
			    ctx, acc, 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.
 *
 */
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;
	uint32_t group_type = 0;
	const struct ldb_message *msg = dsdb_audit_get_message(acc->request);
	if (status == LDB_SUCCESS && msg != NULL) {
		struct ldb_result *res = NULL;
		ret = dsdb_module_search_dn(
			acc->module,
			ctx,
			&res,
			msg->dn,
			group_attrs,
			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");
			group_type = ldb_msg_find_attr_as_uint(
			    res->msgs[0], "groupType", 0);
			log_membership_changes(acc->module,
					       acc->request,
					       new_val,
					       acc->members,
					       group_type,
					       status);
			TALLOC_FREE(ctx);
			return;
		}
	}
	/*
	 * If we get here either
	 *   one of the lower level modules failed and the group record did
	 *   not get updated
	 * or
	 *   the updated group record could not be read.
	 *
	 * In both cases it does not make sense to log individual membership
	 * changes so we log a group membership change "Failure" message.
	 *
	 */
	log_membership_change(acc->module,
	                      acc->request,
			      "Failure",
			      "",
			      EVT_ID_NONE,
			      status);
	TALLOC_FREE(ctx);
}

/*
 * @brief call back function to log changes to the group memberships.
 *
 * Call back function to log changes to the uses broup memberships.
 *
 * @param req the ldb request.
 * @param ares the ldb result
 *
 * @return am LDB status code.
 */
static int group_audit_callback(
	struct ldb_request *req,
	struct ldb_reply *ares)
{
	struct audit_callback_context *ac = NULL;

	ac = talloc_get_type(
		req->context,
		struct audit_callback_context);

	if (!ares) {
		return ldb_module_done(
				ac->request, NULL, NULL,
				LDB_ERR_OPERATIONS_ERROR);
	}

	/* pass on to the callback */
	switch (ares->type) {
	case LDB_REPLY_ENTRY:
		return ldb_module_send_entry(
			ac->request,
			ares->message,
			ares->controls);

	case LDB_REPLY_REFERRAL:
		return ldb_module_send_referral(
			ac->request,
			ares->referral);

	case LDB_REPLY_DONE:
		/*
		 * Log on DONE now we have a result code
		 */
		ac->log_changes(ac, ares->error);
		return ldb_module_done(
			ac->request,
			ares->controls,
			ares->response,
			ares->error);
		break;

	default:
		/* Can't happen */
		return LDB_ERR_OPERATIONS_ERROR;
	}
}

/*
 * @brief Does this request change the primary group.
 *
 * Does the request change the primary group, i.e. does it contain the
 * primaryGroupID attribute.
 *
 * @param req the request to examine.
 *
 * @return True if the request modifies the primary group.
 */
static bool has_primary_group_id(struct ldb_request *req)
{
	struct ldb_message_element *el = NULL;
	const struct ldb_message *msg = NULL;

	msg = dsdb_audit_get_message(req);
	el = ldb_msg_find_element(msg, "primaryGroupID");

	return (el != NULL);
}

/*
 * @brief Does this request change group membership.
 *
 * Does the request change the ses group memberships, i.e. does it contain the
 * member attribute.
 *
 * @param req the request to examine.
 *
 * @return True if the request modifies the users group memberships.
 */
static bool has_group_membership_changes(struct ldb_request *req)
{
	struct ldb_message_element *el = NULL;
	const struct ldb_message *msg = NULL;

	msg = dsdb_audit_get_message(req);
	el = ldb_msg_find_element(msg, "member");

	return (el != NULL);
}



/*
 * @brief Install the callback function to log an add request.
 *
 * Install the callback function to log an add request changing the users
 * group memberships. As we want to log the returned status code, we need to
 * register a callback function that will be called once the operation has
 * completed.
 *
 * This function reads the current user record so that we can log the before
 * and after state.
 *
 * @param module The ldb module.
 * @param req The modify request.
 *
 * @return and LDB status code.
 */
static int set_group_membership_add_callback(
	struct ldb_module *module,
	struct ldb_request *req)
{
	struct audit_callback_context *context = NULL;
	struct ldb_request *new_req = NULL;
	struct ldb_context *ldb = NULL;
	int ret;
	/*
	 * Adding group memberships so will need to log the changes.
	 */
	ldb = ldb_module_get_ctx(module);
	context = talloc_zero(req, struct audit_callback_context);

	if (context == NULL) {
		return ldb_oom(ldb);
	}
	context->request = req;
	context->module = module;
	context->log_changes = log_group_membership_changes;
	/*
	 * We want to log the return code status, so we need to register
	 * a callback function to get the actual result.
	 * We need to take a new copy so that we don't alter the callers copy
	 */
	ret = ldb_build_add_req(
		&new_req,
		ldb,
		req,
		req->op.add.message,
		req->controls,
		context,
		group_audit_callback,
		req);
	if (ret != LDB_SUCCESS) {
		return ret;
	}
	return ldb_next_request(module, new_req);
}


/*
 * @brief Install the callback function to log a modify request.
 *
 * Install the callback function to log a modify request changing the primary
 * group . As we want to log the returned status code, we need to register a
 * callback function that will be called once the operation has completed.
 *
 * This function reads the current user record so that we can log the before
 * and after state.
 *
 * @param module The ldb module.
 * @param req The modify request.
 *
 * @return and LDB status code.
 */
static int set_primary_group_modify_callback(
	struct ldb_module *module,
	struct ldb_request *req)
{
	struct audit_callback_context *context = NULL;
	struct ldb_request *new_req = NULL;
	struct ldb_context *ldb = NULL;
	const struct ldb_message *msg = NULL;
	struct ldb_result *res = NULL;
	int ret;

	TALLOC_CTX *ctx = talloc_new(NULL);

	ldb = ldb_module_get_ctx(module);

	context = talloc_zero(req, struct audit_callback_context);
	if (context == NULL) {
		ret = ldb_oom(ldb);
		goto exit;
	}
	context->request = req;
	context->module = module;
	context->log_changes = log_user_primary_group_change;

	msg = dsdb_audit_get_message(req);
	ret = dsdb_module_search_dn(
		module,
		ctx,
		&res,
		msg->dn,
		primary_group_attr,
		DSDB_FLAG_NEXT_MODULE |
		DSDB_SEARCH_REVEAL_INTERNALS |
		DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT,
		NULL);
	if (ret == LDB_SUCCESS) {
		uint32_t pg;
		pg = ldb_msg_find_attr_as_uint(
			res->msgs[0],
			"primaryGroupID",
			~0);
		context->primary_group = pg;
	}
	/*
	 * We want to log the return code status, so we need to register
	 * a callback function to get the actual result.
	 * We need to take a new copy so that we don't alter the callers copy
	 */
	ret = ldb_build_mod_req(
		&new_req,
		ldb,
		req,
		req->op.add.message,
		req->controls,
		context,
		group_audit_callback,
		req);
	if (ret != LDB_SUCCESS) {
		goto exit;
	}
	ret = ldb_next_request(module, new_req);
exit:
	TALLOC_FREE(ctx);
	return ret;
}

/*
 * @brief Install the callback function to log an add request.
 *
 * Install the callback function to log an add request changing the primary
 * group . As we want to log the returned status code, we need to register a
 * callback function that will be called once the operation has completed.
 *
 * This function reads the current user record so that we can log the before
 * and after state.
 *
 * @param module The ldb module.
 * @param req The modify request.
 *
 * @return and LDB status code.
 */
static int set_primary_group_add_callback(
	struct ldb_module *module,
	struct ldb_request *req)
{
	struct audit_callback_context *context = NULL;
	struct ldb_request *new_req = NULL;
	struct ldb_context *ldb = NULL;
	int ret;
	/*
	 * Adding a user with a primary group.
	 */
	ldb = ldb_module_get_ctx(module);
	context = talloc_zero(req, struct audit_callback_context);

	if (context == NULL) {
		return ldb_oom(ldb);
	}
	context->request = req;
	context->module = module;
	context->log_changes = log_user_primary_group_change;
	/*
	 * We want to log the return code status, so we need to register
	 * a callback function to get the actual result.
	 * We need to take a new copy so that we don't alter the callers copy
	 */
	ret = ldb_build_add_req(
		&new_req,
		ldb,
		req,
		req->op.add.message,
		req->controls,
		context,
		group_audit_callback,
		req);
	if (ret != LDB_SUCCESS) {
		return ret;
	}
	return ldb_next_request(module, new_req);
}

/*
 * @brief Module handler for add operations.
 *
 * Inspect the current add request, and if needed log any group membership
 * changes.
 *
 * @param module The ldb module.
 * @param req The modify request.
 *
 * @return and LDB status code.
 */
static int group_add(
	struct ldb_module *module,
	struct ldb_request *req)
{

	struct audit_context *ac =
		talloc_get_type(
			ldb_module_get_private(module),
			struct audit_context);
	/*
	 * Currently we don't log replicated group changes
	 */
	if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) {
		return ldb_next_request(module, req);
	}

	if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL) ||
		CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) ||
		(ac->msg_ctx && ac->send_events)) {
		/*
		 * Avoid the overheads of logging unless it has been
		 * enabled
		 */
		if (has_group_membership_changes(req)) {
			return set_group_membership_add_callback(module, req);
		}
		if (has_primary_group_id(req)) {
			return set_primary_group_add_callback(module, req);
		}
	}
	return ldb_next_request(module, req);
}

/*
 * @brief Module handler for delete operations.
 *
 * Currently there is no logging for delete operations.
 *
 * @param module The ldb module.
 * @param req The modify request.
 *
 * @return and LDB status code.
 */
static int group_delete(
	struct ldb_module *module,
	struct ldb_request *req)
{
	return ldb_next_request(module, req);
}

/*
 * @brief Install the callback function to log a modify request.
 *
 * Install the callback function to log a modify request. As we want to log the
 * returned status code, we need to register a callback function that will be
 * called once the operation has completed.
 *
 * This function reads the current user record so that we can log the before
 * and after state.
 *
 * @param module The ldb module.
 * @param req The modify request.
 *
 * @return and LDB status code.
 */
static int set_group_modify_callback(
	struct ldb_module *module,
	struct ldb_request *req)
{
	struct audit_callback_context *context = NULL;
	struct ldb_request *new_req = NULL;
	struct ldb_context *ldb = NULL;
	struct ldb_result *res = NULL;
	int ret;

	ldb = ldb_module_get_ctx(module);
	context = talloc_zero(req, struct audit_callback_context);

	if (context == NULL) {
		return ldb_oom(ldb);
	}
	context->request = req;
	context->module  = module;
	context->log_changes = log_group_membership_changes;

	/*
	 * About to change the group memberships need to read
	 * the current state from the database.
	 */
	ret = dsdb_module_search_dn(
		module,
		context,
		&res,
		req->op.add.message->dn,
		group_attrs,
		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 *ev = 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(context,
							  lp_ctx,
							  ev);
	}

	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);
}
