/* 
   Unix SMB/CIFS implementation.
   
   Extract the user/system database from a remote SamSync server

   Copyright (C) Andrew Bartlett <abartlet@samba.org> 2004-2005
   Copyright (C) Andrew Tridgell 2004
   Copyright (C) Volker Lendecke 2004
   
   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 "includes.h"
#include "libnet/libnet.h"
#include "libcli/ldap/ldap_ndr.h"
#include "dsdb/samdb/samdb.h"
#include "auth/auth.h"
#include "../lib/util/util_ldb.h"
#include "librpc/gen_ndr/ndr_misc.h"
#include "ldb_wrap.h"
#include "libcli/security/security.h"
#include "param/param.h"

struct samsync_ldb_secret {
	struct samsync_ldb_secret *prev, *next;
	DATA_BLOB secret;
	char *name;
	NTTIME mtime;
};

struct samsync_ldb_trusted_domain {
	struct samsync_ldb_trusted_domain *prev, *next;
        struct dom_sid *sid;
	char *name;
};

struct samsync_ldb_state {
	/* Values from the LSA lookup */
	const struct libnet_SamSync_state *samsync_state;

	struct dom_sid *dom_sid[3];
	struct ldb_context *sam_ldb, *remote_ldb, *pdb;
	struct ldb_dn *base_dn[3];
	struct samsync_ldb_secret *secrets;
	struct samsync_ldb_trusted_domain *trusted_domains;
};

static NTSTATUS samsync_ldb_add_foreignSecurityPrincipal(TALLOC_CTX *mem_ctx,
							 struct samsync_ldb_state *state,
							 struct dom_sid *sid,
							 struct ldb_dn **fsp_dn,
							 char **error_string)
{
	const char *sidstr = dom_sid_string(mem_ctx, sid);
	/* We assume that ForeignSecurityPrincipals are under the BASEDN of the main domain */
	struct ldb_dn *basedn = samdb_search_dn(state->sam_ldb, mem_ctx,
						state->base_dn[SAM_DATABASE_DOMAIN],
						"(&(objectClass=container)(cn=ForeignSecurityPrincipals))");
	struct ldb_message *msg;
	int ret;

	if (!sidstr) {
		return NT_STATUS_NO_MEMORY;
	}

	if (basedn == NULL) {
		*error_string = talloc_asprintf(mem_ctx, 
						"Failed to find DN for "
						"ForeignSecurityPrincipal container under %s",
						ldb_dn_get_linearized(state->base_dn[SAM_DATABASE_DOMAIN]));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}
	
	msg = ldb_msg_new(mem_ctx);
	if (msg == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	/* add core elements to the ldb_message for the alias */
	msg->dn = basedn;
	if ( ! ldb_dn_add_child_fmt(msg->dn, "CN=%s", sidstr))
		return NT_STATUS_UNSUCCESSFUL;
	
	samdb_msg_add_string(state->sam_ldb, mem_ctx, msg,
			     "objectClass",
			     "foreignSecurityPrincipal");

	*fsp_dn = msg->dn;

	/* create the alias */
	ret = ldb_add(state->sam_ldb, msg);
	if (ret != 0) {
		*error_string = talloc_asprintf(mem_ctx, "Failed to create foreignSecurityPrincipal "
						"record %s: %s",
						ldb_dn_get_linearized(msg->dn),
						ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}
	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_handle_domain(TALLOC_CTX *mem_ctx,
					  struct samsync_ldb_state *state,
					  enum netr_SamDatabaseID database,
					  struct netr_DELTA_ENUM *delta,
					  char **error_string) 
{
	struct netr_DELTA_DOMAIN *domain = delta->delta_union.domain;
	const char *domain_name = domain->domain_name.string;
	struct ldb_message *msg;
	int ret;
	
	msg = ldb_msg_new(mem_ctx);
	if (msg == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	if (database == SAM_DATABASE_DOMAIN) {
		struct ldb_dn *partitions_basedn;
		const char *domain_attrs[] =  {"nETBIOSName", "nCName", NULL};
		struct ldb_message **msgs_domain;
		int ret_domain;

		partitions_basedn = samdb_partitions_dn(state->sam_ldb, mem_ctx);

		ret_domain = gendb_search(state->sam_ldb, mem_ctx, partitions_basedn, &msgs_domain, domain_attrs,
					  "(&(&(nETBIOSName=%s)(objectclass=crossRef))(ncName=*))", 
					  domain_name);
		if (ret_domain == -1) {
			*error_string = talloc_asprintf(mem_ctx, "gendb_search for domain failed: %s", ldb_errstring(state->sam_ldb));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		}
		
		if (ret_domain != 1) {
			*error_string = talloc_asprintf(mem_ctx, "Failed to find existing domain record for %s: %d results", domain_name,
							ret_domain);
			return NT_STATUS_NO_SUCH_DOMAIN;		
		}

		state->base_dn[database] = samdb_result_dn(state->sam_ldb, state, msgs_domain[0], "nCName", NULL);

		if (state->dom_sid[database]) {
			/* Update the domain sid with the incoming
			 * domain (found on LSA pipe, database sid may
			 * be random) */
			samdb_msg_add_dom_sid(state->sam_ldb, mem_ctx, 
					      msg, "objectSid", state->dom_sid[database]);
		} else {
			/* Well, we will have to use the one from the database */
			state->dom_sid[database] = samdb_search_dom_sid(state->sam_ldb, state,
									state->base_dn[database], 
									"objectSid", NULL);
		}

		if (state->samsync_state->domain_guid) {
			struct ldb_val v;
			NTSTATUS status;
			status = GUID_to_ndr_blob(state->samsync_state->domain_guid, msg, &v);
			if (!NT_STATUS_IS_OK(status)) {
				*error_string = talloc_asprintf(mem_ctx, "ndr_push of domain GUID failed!");
				return status;
			}
			
			ldb_msg_add_value(msg, "objectGUID", &v, NULL);
		}
	} else if (database == SAM_DATABASE_BUILTIN) {
		/* work out the builtin_dn - useful for so many calls its worth
		   fetching here */
		const char *dnstring = samdb_search_string(state->sam_ldb, mem_ctx, NULL,
							   "distinguishedName", "objectClass=builtinDomain");
		state->base_dn[database] = ldb_dn_new(state, state->sam_ldb, dnstring);
		if ( ! ldb_dn_validate(state->base_dn[database])) {
			return NT_STATUS_INTERNAL_ERROR;
		}
	} else {
		/* PRIVs DB */
		return NT_STATUS_INVALID_PARAMETER;
	}

	msg->dn = talloc_reference(mem_ctx, state->base_dn[database]);
	if (!msg->dn) {
		return NT_STATUS_NO_MEMORY;
	}

	samdb_msg_add_string(state->sam_ldb, mem_ctx, 
			     msg, "oEMInformation", domain->oem_information.string);

	samdb_msg_add_int64(state->sam_ldb, mem_ctx, 
			    msg, "forceLogoff", domain->force_logoff_time);

	samdb_msg_add_uint(state->sam_ldb, mem_ctx, 
			   msg, "minPwdLen", domain->min_password_length);

	samdb_msg_add_int64(state->sam_ldb, mem_ctx, 
			    msg, "maxPwdAge", domain->max_password_age);

	samdb_msg_add_int64(state->sam_ldb, mem_ctx, 
			    msg, "minPwdAge", domain->min_password_age);

	samdb_msg_add_uint(state->sam_ldb, mem_ctx, 
			   msg, "pwdHistoryLength", domain->password_history_length);

	samdb_msg_add_uint64(state->sam_ldb, mem_ctx, 
			     msg, "modifiedCount", 
			     domain->sequence_num);

	samdb_msg_add_uint64(state->sam_ldb, mem_ctx, 
			     msg, "creationTime", domain->domain_create_time);

	/* TODO: Account lockout, password properties */
	
	ret = dsdb_replace(state->sam_ldb, msg, 0);

	if (ret) {
		return NT_STATUS_INTERNAL_ERROR;
	}
	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_handle_user(TALLOC_CTX *mem_ctx,
					struct samsync_ldb_state *state,
					enum netr_SamDatabaseID database,
					struct netr_DELTA_ENUM *delta,
					char **error_string) 
{
	uint32_t rid = delta->delta_id_union.rid;
	struct netr_DELTA_USER *user = delta->delta_union.user;
	const char *container, *obj_class;
	char *cn_name;
	int cn_name_len;
	const struct dom_sid *user_sid;
	struct ldb_message *msg;
	struct ldb_message **msgs;
	struct ldb_message **remote_msgs = NULL;
	unsigned int i;
	int ret;
	uint32_t acb;
	bool add = false;
	const char *attrs[] = { NULL };
	/* we may change this to a global search, then fill in only the things not in ldap later */
	const char *remote_attrs[] = { "userPrincipalName", "servicePrincipalName", 
				       "msDS-KeyVersionNumber", "objectGUID", NULL};

	user_sid = dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid);
	if (!user_sid) {
		return NT_STATUS_NO_MEMORY;
	}

	msg = ldb_msg_new(mem_ctx);
	if (msg == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	msg->dn = NULL;
	/* search for the user, by rid */
	ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[database],
			   &msgs, attrs, "(&(objectClass=user)(objectSid=%s))", 
			   ldap_encode_ndr_dom_sid(mem_ctx, user_sid));

	if (ret == -1) {
		*error_string = talloc_asprintf(mem_ctx, "LDB for user %s failed: %s", 
						dom_sid_string(mem_ctx, user_sid),
						ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else if (ret == 0) {
		add = true;
	} else if (ret > 1) {
		*error_string = talloc_asprintf(mem_ctx, "More than one user with SID: %s in local LDB", 
						dom_sid_string(mem_ctx, user_sid));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else {
		msg->dn = msgs[0]->dn;
		talloc_steal(msg, msgs[0]->dn);
	}

	/* and do the same on the remote database */
	if (state->remote_ldb) {
		ret = gendb_search(state->remote_ldb, mem_ctx, state->base_dn[database],
				   &remote_msgs, remote_attrs, "(&(objectClass=user)(objectSid=%s))", 
				   ldap_encode_ndr_dom_sid(mem_ctx, user_sid));
		
		if (ret == -1) {
			*error_string = talloc_asprintf(mem_ctx, "remote LDAP for user %s failed: %s", 
							dom_sid_string(mem_ctx, user_sid),
							ldb_errstring(state->remote_ldb));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		} else if (ret == 0) {
			*error_string = talloc_asprintf(mem_ctx, "User exists in samsync but not in remote LDAP domain! (base: %s, SID: %s)", 
							ldb_dn_get_linearized(state->base_dn[database]),
							dom_sid_string(mem_ctx, user_sid));
			return NT_STATUS_NO_SUCH_USER;
		} else if (ret > 1) {
			*error_string = talloc_asprintf(mem_ctx, "More than one user in remote LDAP domain with SID: %s", 
							dom_sid_string(mem_ctx, user_sid));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
			
			/* Try to put things in the same location as the remote server */
		} else if (add) {
			msg->dn = remote_msgs[0]->dn;
			talloc_steal(msg, remote_msgs[0]->dn);
		}
	}

	cn_name   = talloc_strdup(mem_ctx, user->account_name.string);
	NT_STATUS_HAVE_NO_MEMORY(cn_name);
	cn_name_len = strlen(cn_name);

#define ADD_OR_DEL(type, attrib, field) do {				\
		if (user->field) {					\
			samdb_msg_add_ ## type(state->sam_ldb, mem_ctx, msg, \
					       attrib, user->field);	\
		} else if (!add) {					\
			samdb_msg_add_delete(state->sam_ldb, mem_ctx, msg, \
					     attrib);			\
		}							\
        } while (0);

        ADD_OR_DEL(string, "samAccountName", account_name.string);
        ADD_OR_DEL(string, "displayName", full_name.string);

	if (samdb_msg_add_dom_sid(state->sam_ldb, mem_ctx, msg, 
				  "objectSid", dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))) {
		return NT_STATUS_NO_MEMORY; 
	}

        ADD_OR_DEL(uint, "primaryGroupID", primary_gid);
        ADD_OR_DEL(string, "homeDirectory", home_directory.string);
        ADD_OR_DEL(string, "homeDrive", home_drive.string);
        ADD_OR_DEL(string, "scriptPath", logon_script.string);
	ADD_OR_DEL(string, "description", description.string);
	ADD_OR_DEL(string, "userWorkstations", workstations.string);

	ADD_OR_DEL(uint64, "lastLogon", last_logon);
	ADD_OR_DEL(uint64, "lastLogoff", last_logoff);

	if (samdb_msg_add_logon_hours(state->sam_ldb, mem_ctx, msg, "logonHours", &user->logon_hours) != 0) { 
		return NT_STATUS_NO_MEMORY; 
	}

	ADD_OR_DEL(uint, "badPwdCount", bad_password_count);
	ADD_OR_DEL(uint, "logonCount", logon_count);

	ADD_OR_DEL(uint64, "pwdLastSet", last_password_change);
	ADD_OR_DEL(uint64, "accountExpires", acct_expiry);
	
	if (samdb_msg_add_acct_flags(state->sam_ldb, mem_ctx, msg, 
				     "userAccountControl", user->acct_flags) != 0) { 
		return NT_STATUS_NO_MEMORY; 
	} 
	
	if (!add) {
		/* Passwords.  Ensure there is no plaintext stored against
		 * this entry, as we only have hashes */
		samdb_msg_add_delete(state->sam_ldb, mem_ctx, msg,  
				     "userPassword"); 
	}
	if (user->lm_password_present) {
		samdb_msg_add_hash(state->sam_ldb, mem_ctx, msg,  
				   "dBCSPwd", &user->lmpassword);
	} else if (!add) {
		samdb_msg_add_delete(state->sam_ldb, mem_ctx, msg,  
				     "dBCSPwd"); 
	}
	if (user->nt_password_present) {
		samdb_msg_add_hash(state->sam_ldb, mem_ctx, msg,  
				   "unicodePwd", &user->ntpassword);
	} else if (!add) {
		samdb_msg_add_delete(state->sam_ldb, mem_ctx, msg,  
				     "unicodePwd"); 
	}
	    
	ADD_OR_DEL(string, "comment", comment.string);

	if (samdb_msg_add_parameters(state->sam_ldb, mem_ctx, msg, "userParameters", &user->parameters) != 0) {
		return NT_STATUS_NO_MEMORY;
	}

	ADD_OR_DEL(uint, "countryCode", country_code);
	ADD_OR_DEL(uint, "codePage", code_page);

        ADD_OR_DEL(string, "profilePath", profile_path.string);

#undef ADD_OR_DEL

	for (i=0; remote_attrs[i]; i++) {
		struct ldb_message_element *el = ldb_msg_find_element(remote_msgs[0], remote_attrs[i]);
		if (!el) {
			samdb_msg_add_delete(state->sam_ldb, mem_ctx, msg,  
					     remote_attrs[i]); 
		} else {
			ldb_msg_add(msg, el, LDB_FLAG_MOD_REPLACE);
		}
	}

	acb = user->acct_flags;
	if (acb & (ACB_WSTRUST)) {
		cn_name[cn_name_len - 1] = '\0';
		container = "Computers";
		obj_class = "computer";
		
	} else if (acb & ACB_SVRTRUST) {
		if (cn_name[cn_name_len - 1] != '$') {
			return NT_STATUS_FOOBAR;		
		}
		cn_name[cn_name_len - 1] = '\0';
		container = "Domain Controllers";
		obj_class = "computer";
	} else {
		container = "Users";
		obj_class = "user";
	}
	if (add) {
		samdb_msg_add_string(state->sam_ldb, mem_ctx, msg, 
				     "objectClass", obj_class);
		if (!msg->dn) {
			msg->dn = ldb_dn_copy(mem_ctx, state->base_dn[database]);
			ldb_dn_add_child_fmt(msg->dn, "CN=%s,CN=%s", cn_name, container);
			if (!msg->dn) {
				return NT_STATUS_NO_MEMORY;		
			}
		}

		ret = ldb_add(state->sam_ldb, msg);
		if (ret != 0) {
			struct ldb_dn *first_try_dn = msg->dn;
			/* Try again with the default DN */
			if (!remote_msgs) {
				*error_string = talloc_asprintf(mem_ctx, "Failed to create user record.  Tried %s: %s",
								ldb_dn_get_linearized(first_try_dn),
								ldb_errstring(state->sam_ldb));
				return NT_STATUS_INTERNAL_DB_CORRUPTION;
			} else {
				msg->dn = talloc_steal(msg, remote_msgs[0]->dn);
				ret = ldb_add(state->sam_ldb, msg);
				if (ret != 0) {
					*error_string = talloc_asprintf(mem_ctx, "Failed to create user record.  Tried both %s and %s: %s",
									ldb_dn_get_linearized(first_try_dn),
									ldb_dn_get_linearized(msg->dn),
									ldb_errstring(state->sam_ldb));
					return NT_STATUS_INTERNAL_DB_CORRUPTION;
				}
			}
		}
	} else {
		ret = dsdb_replace(state->sam_ldb, msg, 0);
		if (ret != 0) {
			*error_string = talloc_asprintf(mem_ctx, "Failed to modify user record %s: %s",
							ldb_dn_get_linearized(msg->dn),
							ldb_errstring(state->sam_ldb));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		}
	}

	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_delete_user(TALLOC_CTX *mem_ctx,
					struct samsync_ldb_state *state,
					enum netr_SamDatabaseID database,
					struct netr_DELTA_ENUM *delta,
					char **error_string) 
{
	uint32_t rid = delta->delta_id_union.rid;
	struct ldb_message **msgs;
	int ret;
	const char *attrs[] = { NULL };

	/* search for the user, by rid */
	ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[database],
			   &msgs, attrs, "(&(objectClass=user)(objectSid=%s))", 
			   ldap_encode_ndr_dom_sid(mem_ctx, dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))); 

	if (ret == -1) {
		*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else if (ret == 0) {
		return NT_STATUS_NO_SUCH_USER;
	} else if (ret > 1) {
		*error_string = talloc_asprintf(mem_ctx, "More than one user with SID: %s", 
						dom_sid_string(mem_ctx, 
							       dom_sid_add_rid(mem_ctx, 
									       state->dom_sid[database], 
									       rid)));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}

	ret = ldb_delete(state->sam_ldb, msgs[0]->dn);
	if (ret != 0) {
		*error_string = talloc_asprintf(mem_ctx, "Failed to delete user record %s: %s",
						ldb_dn_get_linearized(msgs[0]->dn),
						ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}

	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_handle_group(TALLOC_CTX *mem_ctx,
					 struct samsync_ldb_state *state,
					 enum netr_SamDatabaseID database,
					 struct netr_DELTA_ENUM *delta,
					 char **error_string) 
{
	uint32_t rid = delta->delta_id_union.rid;
	struct netr_DELTA_GROUP *group = delta->delta_union.group;
	const char *container, *obj_class;
	const char *cn_name;

	struct ldb_message *msg;
	struct ldb_message **msgs;
	int ret;
	bool add = false;
	const char *attrs[] = { NULL };

	msg = ldb_msg_new(mem_ctx);
	if (msg == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	/* search for the group, by rid */
	ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[database], &msgs, attrs,
			   "(&(objectClass=group)(objectSid=%s))", 
			   ldap_encode_ndr_dom_sid(mem_ctx, dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))); 

	if (ret == -1) {
		*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else if (ret == 0) {
		add = true;
	} else if (ret > 1) {
		*error_string = talloc_asprintf(mem_ctx, "More than one group/alias with SID: %s", 
						dom_sid_string(mem_ctx, 
							       dom_sid_add_rid(mem_ctx, 
									       state->dom_sid[database], 
									       rid)));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else {
		msg->dn = talloc_steal(msg, msgs[0]->dn);
	}

	cn_name   = group->group_name.string;

#define ADD_OR_DEL(type, attrib, field) do {				\
		if (group->field) {					\
			samdb_msg_add_ ## type(state->sam_ldb, mem_ctx, msg, \
					       attrib, group->field);	\
		} else if (!add) {					\
			samdb_msg_add_delete(state->sam_ldb, mem_ctx, msg, \
					     attrib);			\
		}							\
        } while (0);

        ADD_OR_DEL(string, "samAccountName", group_name.string);

	if (samdb_msg_add_dom_sid(state->sam_ldb, mem_ctx, msg, 
				  "objectSid", dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))) {
		return NT_STATUS_NO_MEMORY; 
	}

	ADD_OR_DEL(string, "description", description.string);

#undef ADD_OR_DEL

	container = "Users";
	obj_class = "group";

	if (add) {
		samdb_msg_add_string(state->sam_ldb, mem_ctx, msg, 
				     "objectClass", obj_class);
		msg->dn = ldb_dn_copy(mem_ctx, state->base_dn[database]);
		ldb_dn_add_child_fmt(msg->dn, "CN=%s,CN=%s", cn_name, container);
		if (!msg->dn) {
			return NT_STATUS_NO_MEMORY;		
		}

		ret = ldb_add(state->sam_ldb, msg);
		if (ret != 0) {
			*error_string = talloc_asprintf(mem_ctx, "Failed to create group record %s: %s",
							ldb_dn_get_linearized(msg->dn),
							ldb_errstring(state->sam_ldb));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		}
	} else {
		ret = dsdb_replace(state->sam_ldb, msg, 0);
		if (ret != 0) {
			*error_string = talloc_asprintf(mem_ctx, "Failed to modify group record %s: %s",
							ldb_dn_get_linearized(msg->dn),
							ldb_errstring(state->sam_ldb));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		}
	}

	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_delete_group(TALLOC_CTX *mem_ctx,
					 struct samsync_ldb_state *state,
					 enum netr_SamDatabaseID database,
					 struct netr_DELTA_ENUM *delta,
					 char **error_string) 
{
	uint32_t rid = delta->delta_id_union.rid;
	struct ldb_message **msgs;
	int ret;
	const char *attrs[] = { NULL };

	/* search for the group, by rid */
	ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[database], &msgs, attrs,
			   "(&(objectClass=group)(objectSid=%s))", 
			   ldap_encode_ndr_dom_sid(mem_ctx, dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))); 

	if (ret == -1) {
		*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else if (ret == 0) {
		return NT_STATUS_NO_SUCH_GROUP;
	} else if (ret > 1) {
		*error_string = talloc_asprintf(mem_ctx, "More than one group/alias with SID: %s", 
						dom_sid_string(mem_ctx, 
							       dom_sid_add_rid(mem_ctx, 
									       state->dom_sid[database], 
									       rid)));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}
	
	ret = ldb_delete(state->sam_ldb, msgs[0]->dn);
	if (ret != 0) {
		*error_string = talloc_asprintf(mem_ctx, "Failed to delete group record %s: %s",
						ldb_dn_get_linearized(msgs[0]->dn),
						ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}

	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_handle_group_member(TALLOC_CTX *mem_ctx,
						struct samsync_ldb_state *state,
						enum netr_SamDatabaseID database,
						struct netr_DELTA_ENUM *delta,
						char **error_string) 
{
	uint32_t rid = delta->delta_id_union.rid;
	struct netr_DELTA_GROUP_MEMBER *group_member = delta->delta_union.group_member;
	struct ldb_message *msg;
	struct ldb_message **msgs;
	int ret;
	const char *attrs[] = { NULL };
	uint32_t i;

	msg = ldb_msg_new(mem_ctx);
	if (msg == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	/* search for the group, by rid */
	ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[database], &msgs, attrs,
			   "(&(objectClass=group)(objectSid=%s))", 
			   ldap_encode_ndr_dom_sid(mem_ctx, dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))); 

	if (ret == -1) {
		*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else if (ret == 0) {
		return NT_STATUS_NO_SUCH_GROUP;
	} else if (ret > 1) {
		*error_string = talloc_asprintf(mem_ctx, "More than one group/alias with SID: %s", 
						dom_sid_string(mem_ctx, 
							       dom_sid_add_rid(mem_ctx, 
									       state->dom_sid[database], 
									       rid)));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else {
		msg->dn = talloc_steal(msg, msgs[0]->dn);
	}
	
	talloc_free(msgs);

	for (i=0; i<group_member->num_rids; i++) {
		/* search for the group, by rid */
		ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[database], &msgs, attrs,
				   "(&(objectClass=user)(objectSid=%s))", 
				   ldap_encode_ndr_dom_sid(mem_ctx, dom_sid_add_rid(mem_ctx, state->dom_sid[database], group_member->rids[i]))); 
		
		if (ret == -1) {
			*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		} else if (ret == 0) {
			return NT_STATUS_NO_SUCH_USER;
		} else if (ret > 1) {
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		} else {
			samdb_msg_add_string(state->sam_ldb, mem_ctx, msg, "member", ldb_dn_alloc_linearized(mem_ctx, msgs[0]->dn));
		}
		
		talloc_free(msgs);
	}
	
	ret = dsdb_replace(state->sam_ldb, msg, 0);
	if (ret != 0) {
		*error_string = talloc_asprintf(mem_ctx, "Failed to modify group record %s: %s",
						ldb_dn_get_linearized(msg->dn),
						ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}

	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_handle_alias(TALLOC_CTX *mem_ctx,
					 struct samsync_ldb_state *state,
					 enum netr_SamDatabaseID database,
					 struct netr_DELTA_ENUM *delta,
					 char **error_string) 
{
	uint32_t rid = delta->delta_id_union.rid;
	struct netr_DELTA_ALIAS *alias = delta->delta_union.alias;
	const char *container, *obj_class;
	const char *cn_name;

	struct ldb_message *msg;
	struct ldb_message **msgs;
	int ret;
	bool add = false;
	const char *attrs[] = { NULL };

	msg = ldb_msg_new(mem_ctx);
	if (msg == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	/* search for the alias, by rid */
	ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[database], &msgs, attrs,
			   "(&(objectClass=group)(objectSid=%s))", 
			   ldap_encode_ndr_dom_sid(mem_ctx, dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))); 

	if (ret == -1) {
		*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else if (ret == 0) {
		add = true;
	} else if (ret > 1) {
		*error_string = talloc_asprintf(mem_ctx, "More than one group/alias with SID: %s", 
						dom_sid_string(mem_ctx, 
							       dom_sid_add_rid(mem_ctx, 
									       state->dom_sid[database], 
									       rid)));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else {
		msg->dn = talloc_steal(mem_ctx, msgs[0]->dn);
	}

	cn_name   = alias->alias_name.string;

#define ADD_OR_DEL(type, attrib, field) do {				\
		if (alias->field) {					\
			samdb_msg_add_ ## type(state->sam_ldb, mem_ctx, msg, \
					       attrib, alias->field);	\
		} else if (!add) {					\
			samdb_msg_add_delete(state->sam_ldb, mem_ctx, msg, \
					     attrib);			\
		}							\
	} while (0);

	ADD_OR_DEL(string, "samAccountName", alias_name.string);

	if (samdb_msg_add_dom_sid(state->sam_ldb, mem_ctx, msg, 
				  "objectSid", dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))) {
		return NT_STATUS_NO_MEMORY; 
	}

	ADD_OR_DEL(string, "description", description.string);

#undef ADD_OR_DEL

	samdb_msg_add_uint(state->sam_ldb, mem_ctx, msg, "groupType", 0x80000004);

	container = "Users";
	obj_class = "group";

	if (add) {
		samdb_msg_add_string(state->sam_ldb, mem_ctx, msg, 
				     "objectClass", obj_class);
		msg->dn = ldb_dn_copy(mem_ctx, state->base_dn[database]);
		ldb_dn_add_child_fmt(msg->dn, "CN=%s,CN=%s", cn_name, container);
		if (!msg->dn) {
			return NT_STATUS_NO_MEMORY;		
		}

		ret = ldb_add(state->sam_ldb, msg);
		if (ret != 0) {
			*error_string = talloc_asprintf(mem_ctx, "Failed to create alias record %s: %s",
							ldb_dn_get_linearized(msg->dn),
							ldb_errstring(state->sam_ldb));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		}
	} else {
		ret = dsdb_replace(state->sam_ldb, msg, 0);
		if (ret != 0) {
			*error_string = talloc_asprintf(mem_ctx, "Failed to modify alias record %s: %s",
							ldb_dn_get_linearized(msg->dn),
							ldb_errstring(state->sam_ldb));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		}
	}

	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_delete_alias(TALLOC_CTX *mem_ctx,
					 struct samsync_ldb_state *state,
					 enum netr_SamDatabaseID database,
					 struct netr_DELTA_ENUM *delta,
					 char **error_string) 
{
	uint32_t rid = delta->delta_id_union.rid;
	struct ldb_message **msgs;
	int ret;
	const char *attrs[] = { NULL };

	/* search for the alias, by rid */
	ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[database], &msgs, attrs,
			   "(&(objectClass=group)(objectSid=%s))", 
			   ldap_encode_ndr_dom_sid(mem_ctx, dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))); 

	if (ret == -1) {
		*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else if (ret == 0) {
		return NT_STATUS_NO_SUCH_ALIAS;
	} else if (ret > 1) {
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}

	ret = ldb_delete(state->sam_ldb, msgs[0]->dn);
	if (ret != 0) {
		*error_string = talloc_asprintf(mem_ctx, "Failed to delete alias record %s: %s",
						ldb_dn_get_linearized(msgs[0]->dn),
						ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}

	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_handle_alias_member(TALLOC_CTX *mem_ctx,
						struct samsync_ldb_state *state,
						enum netr_SamDatabaseID database,
						struct netr_DELTA_ENUM *delta,
						char **error_string) 
{
	uint32_t rid = delta->delta_id_union.rid;
	struct netr_DELTA_ALIAS_MEMBER *alias_member = delta->delta_union.alias_member;
	struct ldb_message *msg;
	struct ldb_message **msgs;
	int ret;
	const char *attrs[] = { NULL };
	uint32_t i;

	msg = ldb_msg_new(mem_ctx);
	if (msg == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	/* search for the alias, by rid */
	ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[database], &msgs, attrs,
			   "(&(objectClass=group)(objectSid=%s))", 
			   ldap_encode_ndr_dom_sid(mem_ctx, dom_sid_add_rid(mem_ctx, state->dom_sid[database], rid))); 
		
	if (ret == -1) {
		*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else if (ret == 0) {
		return NT_STATUS_NO_SUCH_GROUP;
	} else if (ret > 1) {
		*error_string = talloc_asprintf(mem_ctx, "More than one group/alias with SID: %s", 
						dom_sid_string(mem_ctx, 
							       dom_sid_add_rid(mem_ctx, 
									       state->dom_sid[database], 
									       rid)));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else {
		msg->dn = talloc_steal(msg, msgs[0]->dn);
	}
	
	talloc_free(msgs);

	for (i=0; i<alias_member->sids.num_sids; i++) {
		struct ldb_dn *alias_member_dn;
		/* search for members, in the top basedn (normal users are builtin aliases) */
		ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[SAM_DATABASE_DOMAIN], &msgs, attrs,
				   "(objectSid=%s)", 
				   ldap_encode_ndr_dom_sid(mem_ctx, alias_member->sids.sids[i].sid)); 

		if (ret == -1) {
			*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		} else if (ret == 0) {
			NTSTATUS nt_status;
			nt_status = samsync_ldb_add_foreignSecurityPrincipal(mem_ctx, state,
									     alias_member->sids.sids[i].sid, 
									     &alias_member_dn, 
									     error_string);
			if (!NT_STATUS_IS_OK(nt_status)) {
				return nt_status;
			}
		} else if (ret > 1) {
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		} else {
			alias_member_dn = msgs[0]->dn;
		}
		samdb_msg_add_string(state->sam_ldb, mem_ctx, msg, "member", ldb_dn_alloc_linearized(mem_ctx, alias_member_dn));
	
		talloc_free(msgs);
	}

	ret = dsdb_replace(state->sam_ldb, msg, 0);
	if (ret != 0) {
		*error_string = talloc_asprintf(mem_ctx, "Failed to modify group record %s: %s",
						ldb_dn_get_linearized(msg->dn),
						ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}

	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_handle_account(TALLOC_CTX *mem_ctx,
					   struct samsync_ldb_state *state,
					   enum netr_SamDatabaseID database,
					   struct netr_DELTA_ENUM *delta,
					   char **error_string) 
{
	struct dom_sid *sid = delta->delta_id_union.sid;
	struct netr_DELTA_ACCOUNT *account = delta->delta_union.account;

	struct ldb_message *msg;
	int ret;
	uint32_t i;
	char *dnstr, *sidstr;

	msg = ldb_msg_new(mem_ctx);
	if (msg == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	sidstr = dom_sid_string(msg, sid);
	NT_STATUS_HAVE_NO_MEMORY_AND_FREE(sidstr, msg);

	dnstr = talloc_asprintf(msg, "sid=%s", sidstr);
	NT_STATUS_HAVE_NO_MEMORY_AND_FREE(dnstr, msg);

	msg->dn = ldb_dn_new(msg, state->pdb, dnstr);
	NT_STATUS_HAVE_NO_MEMORY_AND_FREE(msg->dn, msg);

	for (i=0; i< account->privilege_entries; i++) {
		samdb_msg_add_string(state->pdb, mem_ctx, msg, "privilege",
				     account->privilege_name[i].string);
	}

	ret = dsdb_replace(state->pdb, msg, 0);
	if (ret == LDB_ERR_NO_SUCH_OBJECT) {
		if (samdb_msg_add_dom_sid(state->pdb, msg, msg, "objectSid", sid) != LDB_SUCCESS) {
			talloc_free(msg);
			return NT_STATUS_NO_MEMORY;
		}
		samdb_msg_add_string(state->pdb, msg, msg, "comment", "added via samsync");
		ret = ldb_add(state->pdb, msg);		
	}

	if (ret != 0) {
		*error_string = talloc_asprintf(mem_ctx, "Failed to modify privilege record %s",
						ldb_dn_get_linearized(msg->dn));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}

	return NT_STATUS_OK;
}

static NTSTATUS samsync_ldb_delete_account(TALLOC_CTX *mem_ctx,
					   struct samsync_ldb_state *state,
					   enum netr_SamDatabaseID database,
					   struct netr_DELTA_ENUM *delta,
					   char **error_string) 
{
	struct dom_sid *sid = delta->delta_id_union.sid;

	struct ldb_message *msg;
	struct ldb_message **msgs;
	int ret;
	const char *attrs[] = { NULL };

	msg = ldb_msg_new(mem_ctx);
	if (msg == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	/* search for the account, by sid, in the top basedn */
	ret = gendb_search(state->sam_ldb, mem_ctx, state->base_dn[SAM_DATABASE_DOMAIN], &msgs, attrs,
			   "(objectSid=%s)", 
			   ldap_encode_ndr_dom_sid(mem_ctx, sid)); 

	if (ret == -1) {
		*error_string = talloc_asprintf(mem_ctx, "gendb_search failed: %s", ldb_errstring(state->sam_ldb));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else if (ret == 0) {
		return NT_STATUS_NO_SUCH_USER;
	} else if (ret > 1) {
		*error_string = talloc_asprintf(mem_ctx, "More than one account with SID: %s", 
						dom_sid_string(mem_ctx, sid));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	} else {
		msg->dn = talloc_steal(msg, msgs[0]->dn);
	}

	samdb_msg_add_delete(state->sam_ldb, mem_ctx, msg,  
			     "privilege");

	ret = dsdb_replace(state->sam_ldb, msg, 0);
	if (ret != 0) {
		*error_string = talloc_asprintf(mem_ctx, "Failed to modify privilege record %s",
						ldb_dn_get_linearized(msg->dn));
		return NT_STATUS_INTERNAL_DB_CORRUPTION;
	}

	return NT_STATUS_OK;
}

static NTSTATUS libnet_samsync_ldb_fn(TALLOC_CTX *mem_ctx, 		
				      void *private_data,
				      enum netr_SamDatabaseID database,
				      struct netr_DELTA_ENUM *delta,
				      char **error_string)
{
	NTSTATUS nt_status = NT_STATUS_OK;
	struct samsync_ldb_state *state = talloc_get_type(private_data, struct samsync_ldb_state);

	*error_string = NULL;
	switch (delta->delta_type) {
	case NETR_DELTA_DOMAIN:
	{
		nt_status = samsync_ldb_handle_domain(mem_ctx, 
						      state,
						      database,
						      delta,
						      error_string);
		break;
	}
	case NETR_DELTA_USER:
	{
		nt_status = samsync_ldb_handle_user(mem_ctx, 
						    state,
						    database,
						    delta,
						    error_string);
		break;
	}
	case NETR_DELTA_DELETE_USER:
	{
		nt_status = samsync_ldb_delete_user(mem_ctx, 
						    state,
						    database,
						    delta,
						    error_string);
		break;
	}
	case NETR_DELTA_GROUP:
	{
		nt_status = samsync_ldb_handle_group(mem_ctx, 
						     state,
						     database,
						     delta,
						     error_string);
		break;
	}
	case NETR_DELTA_DELETE_GROUP:
	{
		nt_status = samsync_ldb_delete_group(mem_ctx, 
						     state,
						     database,
						     delta,
						     error_string);
		break;
	}
	case NETR_DELTA_GROUP_MEMBER:
	{
		nt_status = samsync_ldb_handle_group_member(mem_ctx, 
							    state,
							    database,
							    delta,
							    error_string);
		break;
	}
	case NETR_DELTA_ALIAS:
	{
		nt_status = samsync_ldb_handle_alias(mem_ctx, 
						     state,
						     database,
						     delta,
						     error_string);
		break;
	}
	case NETR_DELTA_DELETE_ALIAS:
	{
		nt_status = samsync_ldb_delete_alias(mem_ctx, 
						     state,
						     database,
						     delta,
						     error_string);
		break;
	}
	case NETR_DELTA_ALIAS_MEMBER:
	{
		nt_status = samsync_ldb_handle_alias_member(mem_ctx, 
							    state,
							    database,
							    delta,
							    error_string);
		break;
	}
	case NETR_DELTA_ACCOUNT:
	{
		nt_status = samsync_ldb_handle_account(mem_ctx, 
						       state,
						       database,
						       delta,
						       error_string);
		break;
	}
	case NETR_DELTA_DELETE_ACCOUNT:
	{
		nt_status = samsync_ldb_delete_account(mem_ctx, 
						       state,
						       database,
						       delta,
						       error_string);
		break;
	}
	default:
		/* Can't dump them all right now */
		break;
	}
	if (!NT_STATUS_IS_OK(nt_status) && !*error_string) {
		*error_string = talloc_asprintf(mem_ctx, "Failed to handle samsync delta: %s", nt_errstr(nt_status));
	}
	return nt_status;
}

static NTSTATUS libnet_samsync_ldb_init(TALLOC_CTX *mem_ctx, 		
					void *private_data,
					struct libnet_SamSync_state *samsync_state,
					char **error_string)
{
	struct samsync_ldb_state *state = talloc_get_type(private_data, struct samsync_ldb_state);
	const char *server = dcerpc_server_name(samsync_state->netlogon_pipe);
	char *ldap_url;

	state->samsync_state = samsync_state;

	ZERO_STRUCT(state->dom_sid);
	if (state->samsync_state->domain_sid) {
		state->dom_sid[SAM_DATABASE_DOMAIN] = dom_sid_dup(state, state->samsync_state->domain_sid);
	}

	state->dom_sid[SAM_DATABASE_BUILTIN] = dom_sid_parse_talloc(state, SID_BUILTIN);

	if (state->samsync_state->realm) {
		if (!server || !*server) {
			/* huh?  how do we not have a server name?  */
			*error_string = talloc_strdup(mem_ctx, "No DCE/RPC server name available.  How did we connect?");
			return NT_STATUS_INVALID_PARAMETER;
		}
		ldap_url = talloc_asprintf(state, "ldap://%s", server);
		
		state->remote_ldb = ldb_wrap_connect(mem_ctx, 
						     state->samsync_state->machine_net_ctx->event_ctx,
						     state->samsync_state->machine_net_ctx->lp_ctx, 
						     ldap_url, 
						     NULL, state->samsync_state->machine_net_ctx->cred,
						     0);
		if (!state->remote_ldb) {
			*error_string = talloc_asprintf(mem_ctx, "Failed to connect to remote LDAP server at %s (used to extract additional data in SamSync replication)", ldap_url);
			return NT_STATUS_NO_LOGON_SERVERS;
		}
	} else {
		state->remote_ldb = NULL;
	}
	return NT_STATUS_OK;
}

NTSTATUS libnet_samsync_ldb(struct libnet_context *ctx, TALLOC_CTX *mem_ctx, struct libnet_samsync_ldb *r)
{
	NTSTATUS nt_status;
	struct libnet_SamSync r2;
	struct samsync_ldb_state *state = talloc(mem_ctx, struct samsync_ldb_state);

	if (!state) {
		return NT_STATUS_NO_MEMORY;
	}

	state->secrets         = NULL;
	state->trusted_domains = NULL;

	state->sam_ldb         = samdb_connect(mem_ctx, 
					       ctx->event_ctx,
					       ctx->lp_ctx, 
					       r->in.session_info);
	if (!state->sam_ldb) {
		return NT_STATUS_INTERNAL_DB_ERROR;
	}

	state->pdb             = privilege_connect(mem_ctx, 
						   ctx->event_ctx,
						   ctx->lp_ctx);
	if (!state->pdb) {
		return NT_STATUS_INTERNAL_DB_ERROR;
	}

	r2.out.error_string    = NULL;
	r2.in.binding_string   = r->in.binding_string;
	r2.in.init_fn          = libnet_samsync_ldb_init;
	r2.in.delta_fn         = libnet_samsync_ldb_fn;
	r2.in.fn_ctx           = state;
	r2.in.machine_account  = NULL; /* TODO:  Create a machine account, fill this in, and the delete it */
	nt_status              = libnet_SamSync_netlogon(ctx, state, &r2);
	r->out.error_string    = r2.out.error_string;
	talloc_steal(mem_ctx, r->out.error_string);

	if (!NT_STATUS_IS_OK(nt_status)) {
		talloc_free(state);
		return nt_status;
	}
	talloc_free(state);
	return nt_status;
}