#!/usr/bin/perl -w
#
#   LDAPSMB
#
#   Copyright (C) Guenther Deschner 2003-2005
#   Copyright (C) Lars Müller 2003
#   Copyright (C) Björn Jacke 2006
#   
#   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 2 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, write to the Free Software
#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

use strict;
#use utf8;
use Net::LDAP;
use Net::LDAP::Util qw(ldap_error_name ldap_error_text);
use Getopt::Long;
use Pod::Usage;
use POSIX qw(strftime);
use Encode qw(from_to encode_utf8 decode);
use Sys::Syslog;

my ($HAVE_LDAPI, $HAVE_CRYPT_SMBHASH, $HAVE_NET_LDAPS);

BEGIN {
	$HAVE_LDAPI = 0;
	$HAVE_CRYPT_SMBHASH = 0;
	$HAVE_NET_LDAPS = 0;
	if ( eval "require 'Crypt/SmbHash';" ) {
		$HAVE_CRYPT_SMBHASH = 1;
	}
	if ( eval "require Net::LDAPS;" ) {
		$HAVE_NET_LDAPS = 1;
	}
	if ( eval "Net::LDAP->VERSION(0.28);" ) {
		$HAVE_LDAPI = 1;
	}
}

sub True { return 1 };
sub False { return 0 };

my ($SMB_OC, $passdb);
our ($mesg,$entry);
my (%progs, %confs);
my ($ascii_group, $ascii_user);

################
# parse switches
my (	$opt_add,
	$opt_bindpw,
	$opt_comment,
	$opt_delete,
	$opt_debug,
	$opt_force,
	$opt_gid,
	$opt_group,
	$opt_help,
	$opt_homedir,
	$opt_init,
	$opt_join,
	$opt_list,
	$opt_logfile,
	$opt_loginShell,
	$opt_makehomedir,
	$opt_modify,
	$opt_mode,
	$opt_ntgroup, #unused
	$opt_passwd,
	$opt_quiet,
	$opt_raw,
	$opt_remove,
	$opt_rid, # TODO: should also work with usr/wks accounts?
	$opt_showconfig,
	$opt_skeldir,
	$opt_smbacct,
	$opt_smbconfig,
	$opt_uid,
	$opt_user,
	$opt_username,
	$opt_verbose,
	$opt_version,
	$opt_workstation,
	);
# ugly hack, to be removed
$opt_smbconfig = "";

GetOptions(
	'add|a'			=> \$opt_add,
	'bindpw=s'		=> \$opt_bindpw,
	'comment|c=s'		=> \$opt_comment,
	'config'		=> \$opt_showconfig,
	'debug=s'		=> \$opt_debug,
	'delete|d'		=> \$opt_delete,
	'force'			=> \$opt_force,
	'gid|gidnumber:s'	=> \$opt_gid,
	'rid:s'			=> \$opt_rid,
	'group|g:s'		=> \$opt_group,
	'help|?|h'		=> \$opt_help, 
	'homedir=s'		=> \$opt_homedir,
	'init'			=> \$opt_init,
	'join|j'		=> \$opt_join,
	'list|l'		=> \$opt_list,
	'logfile=s'		=> \$opt_logfile,
	'makehomedir'		=> \$opt_makehomedir,
	'modify|m'		=> \$opt_modify,
	'mode=s'		=> \$opt_mode,
	'ntgroup=s'		=> \$opt_ntgroup, #unused
	'passwd|p=s'		=> \$opt_passwd,
	'quiet'			=> \$opt_quiet,
	'raw'			=> \$opt_raw,
	'remove|r'		=> \$opt_remove,
	'shell=s'		=> \$opt_loginShell,
	'skeldir|k=s'		=> \$opt_skeldir,
	'smbacct|s'		=> \$opt_smbacct,
	'smbconf=s'		=> \$opt_smbconfig,
	'uid|uidnumber=i'	=> \$opt_uid,
	'user|u:s'		=> \$opt_user,
	'username:s'		=> \$opt_username,
	'verbose|v'		=> \$opt_verbose,
	'version'		=> \$opt_version,
	'workstation|wks:s'	=> \$opt_workstation,
	) or pod2usage(2);



###############
# some initials

my $VERSION		=	"1.34b";
my $LOGFILE_NAME	=	$opt_logfile || "/var/log/samba/log.ldapsmb";
my $DEBUGLEVEL 		= 	$opt_debug || 0 ;
my $DEBUG_PASSWORD 	= 	0;

my $LDAPSMBRC		=	$ENV{'HOME'}."/.ldapsmbrc";

# leave empty for (expensive) autodetection
my $ADMIN_DN		=	"";
my $ADMIN_PW		=	"";
my $LDAP_PORT		=	389;
my $LDAP_SERVER		=	"localhost";
my $LDAP_SSL		=	"";
my $START_TLS		=	"";
my $SUFFIX		=	"";
my $SUFFIX_GROUPS	=	"";
my $SUFFIX_IDMAP	=	"";
my $SUFFIX_MACHINES	=	"";
my $SUFFIX_USERS	=	"";
my $WORKGROUP		=	"";

my $HOMEDIR_ROOT	=	"/home";
my $SKELETONDIR		=	"/etc/skel";
my $UID_RANGE		=	"1000-200000";
my $GID_RANGE		=	"1000-200000";
my $PRIMARYGIDNUMBER	=	$opt_gid || 100;
my $PRIMARYGIDNUMBER_WKS= 	$opt_gid || 65534;
my $GECOS		=	"SMBLDAP-User";
my $STRUCT_OC_POSIX_ACCOUNT = 	"account";
my $STRUCT_OC_POSIX_GROUP = 	"namedObject";

# this stores maxuid and maxgid-fields in ldap
my $USE_MAXID		=	1;

# FIXME: this need to be autodetected
my $USE_LOGFILE		=	0;

# shall we log to syslog?
my $USE_SYSLOG		=	0;

# use LDAP over IPC
# this will only work with perl-ldap > 0.28
my $USE_LDAPI		=	0;
my $LDAPI_SOCKET	=	"/var/run/slapd/run/ldapi";

# if set to 1, unassigned passwords become the same as the username
my $USE_TRIVIAL_PWD	=	0;

# not yet...
my $HAVE_UTF8		=	0;

my $SMB_CONF;
my $SAMBA_VERSION = "3_0";
my $LDAP_IS_NDS;
my $SAMBA_22;
my $SAMBA_30 = 1;

my $NSCD_SOCKET 	= "/var/run/nscd/socket";
my $NSCD_INIT_SCRIPT 	= "/etc/init.d/nscd";

my @progs = qw( tdbdump pdbedit smbpasswd smbd testparm );
my @confs = qw( smb.conf secrets.tdb );

sub get_samba_version($$$) {

	my $ver = shift;
	my $hd = shift;
	my $schema = shift;

	if ($ver) {
		return;
	}

	$ver = get_smbd_samba_ver($progs{smbd});
	if (!$ver && $hd) {
		$ver = get_schema_samba_ver($hd, $schema);
	};
	if (!$ver) {
		return;
	}
	if ( $ver eq "2_2" ) {
		$SAMBA_22 = 1;
		$SMB_OC = "sambaAccount";
	} elsif ( $ver eq "3_0" or "HEAD" ) {
		$SAMBA_30 = 1;
		$SMB_OC = "sambaSamAccount";
	}
	$SAMBA_VERSION = $ver;
}

# try to detect from LDAP schema
sub detect_schema_from_ldap($) {

	if ($SAMBA_VERSION && $STRUCT_OC_POSIX_ACCOUNT && $STRUCT_OC_POSIX_GROUP) {
		return;
	}

	my $hd = shift || die "no handle yet";
	my $schema = $hd->schema();

	# try to detect samba-version by schema-detection
	if (!$SAMBA_VERSION) {
		$SAMBA_VERSION = get_samba_version(undef, $hd, $schema);
	}

	if (!$STRUCT_OC_POSIX_ACCOUNT) {
		$STRUCT_OC_POSIX_ACCOUNT = get_schema_struct_user_oc($hd, $schema);
	}
	debug(10,"structural account objectclass is: $STRUCT_OC_POSIX_ACCOUNT\n");

	if (!$STRUCT_OC_POSIX_GROUP) {
		$STRUCT_OC_POSIX_GROUP = get_schema_struct_group_oc($hd, $schema);
	}
	debug(10,"structural group objectclass is: $STRUCT_OC_POSIX_GROUP\n");
}

sub setup_configfile() {

	if ( $opt_mode eq "local" ) {
		$SMB_CONF = $opt_smbconfig || $SMB_CONF;
	} elsif ( $opt_mode eq "remote" ) {
		if ( ! $opt_smbconfig || ! -e $opt_smbconfig ) {
			debug(0,"running in remote-mode: need a smb.conf.\n");
			debugadd(0,"please use --smbconf=/my/path/smb.conf\n");
			return False;
		}
		$SMB_CONF = $opt_smbconfig;
	}
	return True;
}


sub find_files {

	if ( $opt_mode eq "local" ) {
		if (!find_progs(@progs)) {
			print "missing binaries\n";
			return False;
		}
		if (!find_confs(@confs)) {
			print "missing configuration\n";
			return False;
		}

		$SMB_CONF = $confs{"smb.conf"} || "";

		find_progs("net") if ($SAMBA_30);
	}
	return True;
}


sub check_ldap_pwd {

	if ($ADMIN_PW) {
		return True;
	}

	if ($opt_bindpw) {
		$ADMIN_PW = $opt_bindpw;
		return True;
	}

	if ($> == 0) {
		$ADMIN_PW = find_adminpwd_in_tdb();
	}
	if ($ADMIN_PW eq "")  {
		debug(0,"sorry. you're not root. please set up your password in $LDAPSMBRC or directly in $0\n");
		return False;
	}
	return True;
}

##################
# execute switches
##################

sub check_syntax() {

	# check syntax
	if ( $opt_add ) {
		if ( !$opt_user && !$opt_group && !$opt_workstation ) {
			debug(0,"error: what do you want to add?\n");
			debugadd(0,"choose between: user, machine or group. exiting.\n");
			return False;
		}
	}

	if ( $opt_modify ) {
		if ( !$opt_user && !$opt_group && !$opt_workstation ) {
			debug(0,"error: what do you want to modify?\n");
			debugadd(0,"choose between: user, machine or group. exiting.\n");
			return False;
		}
	}

	if ( $opt_delete ) {
#	if ( defined($opt_delete) && ($opt_delete eq "") ) {
		if ( !$opt_user && !$opt_group && !$opt_workstation ) {
			debug(0,"error: what do you want to delete?\n");
			debugadd(0,"choose between: user, machine or group. exiting.\n");
			return False;
		}
	}

	if ( $opt_list ) {
		if ( !defined($opt_user) && !defined($opt_group) && !defined($opt_workstation) ) {
			debug(0,"error: what do you want to list?\n");
			debugadd(0,"choose between: user, machine or group. exiting.\n");
			return False;
		}
	}

	if ( $opt_join ) {
		if ( !$opt_user && !$opt_group ) {
			debug(0,"error: you only can join a user to a group. exiting.\n");
			return False;
		}
	}

	if ( $opt_remove ) {
		if ( !$opt_user && !$opt_group ) {
			debug(0,"error: you only can remove a user from a group. exiting.\n");
			return False;
		}
	}

        if ( !defined($opt_add) && !defined($opt_modify) && !defined($opt_delete) &&
	     !defined($opt_list) && !defined($opt_remove) && !defined($opt_join) ) {
		pod2usage(1);
		return False;
        }
	return True;
}


sub init() {

	init_logging();

	if (!detect_mode($opt_mode)) {
		return False;
	}

	if (!setup_configfile()) {
		return False;
	}

	if (!find_files()) {
		return False;
	}

	if (!parse_smbconf()) {
		return False;
	}

	if (!check_ldap_pwd()) {
		return False;
	}

	if (!get_samba_version(undef,undef,undef)) {
		return False;
	}

	return True;
}


sub main {

	# show version
	if ( $opt_version ) {
		print "ldapsmb Version $VERSION\n";
		exit 0;
	}

	# show help
	if ( $opt_help ) {
		pod2usage(1);
		exit 0;
	}
	
	# show config
	if ( $opt_showconfig ) {
		if (!init()) {
			exit 1;
		}
		my $ldap = ldap_anon_bind();
		if ($ldap) {
			detect_schema_from_ldap($ldap);
			print_smb_conf($ldap);
			ldap_unbind($ldap);
		} else {
			print_smb_conf();
		}
		exit 0;
	}

	# init
	if ( $opt_init ) {
		if (!init()) {
			exit 1;
		}
		my $ldap = ldap_bind();
		if (!$ldap) {
			exit 1;
		}
		ldap_init($ldap);
		ldap_unbind($ldap);
		exit 0;
	}

	if (!check_syntax()) {
		exit 1;
	}

	#################################
	#### from here on we do something
	#################################

	if (!init()) {
		exit 1;
	}

	my $ldap = ldap_bind();
	if (!$ldap) {
		exit 1;
	}

	detect_schema_from_ldap($ldap);

	if (!prepare_account($ldap)) {
		goto failed;
	}

	# add user
	if ( $opt_add && $opt_user ) {
		my $USER_FILTER = sprintf "(&(|(objectclass=%s)(objectclass=%s))(uid=%s))",
			"posixAccount", $SMB_OC, $opt_user;
		if ( ldap_search($ldap, $SUFFIX_USERS, $USER_FILTER) ) {
			if ( $opt_smbacct ) {
				debug(0,"adding sambaaccount to existing user: [$opt_user]\n");
				if (!ldap_smbuser_add($opt_user)) {
					goto failed;
				}
				goto done;
			}
			debug(0,"user [$opt_user] already exists. exiting\n");
			goto failed;
		} else {
			if (!$opt_uid) { 
				$opt_uid = ldap_uid($ldap); 
			}
			if ( !$opt_uid ) {
				debug(0,"please define uidNumber with --uid=uidNumber\n");
				goto failed;
			}
			debug(0,"adding user: [$opt_user] (with uidNumber: $opt_uid)\n");
			if (!ldap_posixuser_add($ldap, $opt_user, $opt_uid, $opt_gid)) {
				goto failed;
			}
			if ( $opt_smbacct ) {
				debug(0,"adding sambaaccount: [$opt_user]\n");
				if (!ldap_smbuser_add($opt_user)) {
					goto failed;
				}
			}
		}
	};


	# add workstation
	if ( $opt_add && $opt_workstation ) {
		my $USER_FILTER="(&(objectclass=posixAccount)(uid=$opt_workstation))";
		if ( ldap_search($ldap, $SUFFIX_MACHINES, $USER_FILTER) ) {
			if ( $opt_smbacct ) {
				debug(3,"adding machine-account: [$opt_workstation]\n");
				if (!ldap_smbwks_add($opt_workstation)) {
					goto failed;
				}
				goto done;
			}
			debug(0,"machine [$opt_workstation] already exists. exiting.\n");
			goto failed;
		} else {
			if (!$opt_uid) { 
				$opt_uid = ldap_uid($ldap);
			} 
			if ( !$opt_uid ) {
				debug(0,"please define uidNumber with --uid=uidNumber\n");
				goto failed;
			}
			debug(0,"adding machine: [$opt_workstation] (with uidNumber: $opt_uid)\n");
			if (!ldap_posixwks_add($ldap, $opt_workstation, $opt_uid)) {
				goto failed;
			}
			if ( $opt_smbacct ) {
				debug(0,"adding machine-account: [$opt_workstation]\n");
				if (!ldap_smbwks_add($opt_workstation)) {
					goto failed;
				}
			} 
		}
	};

	# modify user or workstation ("set primary group script" makes no difference)
	if ( $opt_modify && $opt_user ) {

		my $USER_FILTER="(&(objectclass=posixAccount)(uid=$opt_user))";
		my $num_result = ldap_search($ldap, $SUFFIX_USERS, $USER_FILTER);
		if ($num_result == 1) {
			debug(0,"modifying user: [$opt_user]\n");
			if (!ldap_usermodify($ldap, $opt_user)) {
				goto failed;
			}
			goto done;
		}
		$num_result = ldap_search($ldap, $SUFFIX_MACHINES, $USER_FILTER);
		if ($num_result == 1) {
			debug(0,"modifying workstation: [$opt_user]\n");
			if (!ldap_usermodify($ldap, $opt_user)) {
				goto failed;
			}
			goto done;
		} 
		debug(0,"no such account [$opt_user] to modify\n");
		goto failed;
	};


	# list user
	if ( $opt_list && defined($opt_user) ) {

		my $no_user = $opt_user;
		$opt_user = $opt_user || "*";
		my $USER_FILTER = sprintf("(&(objectclass=%s)(uid=%s))", "posixAccount", $opt_user);
		
		if ( ldap_search($ldap, $SUFFIX_USERS, $USER_FILTER) ) {
			debug(5,"list user: [$opt_user]\n");
			if (defined($opt_smbacct)) {
				if (!ldap_list_smbacct($ldap, $opt_user, "user")) {
					goto failed;
				}
			} else {
				if (!ldap_list_user($ldap, $opt_user, "user")) {
					goto failed;
				}
			}
		} elsif ( $no_user eq "") {
			debug(0,"no users found with samba account found in database\n");
			goto done;
		} else {
			debug(0,"no user [$opt_user] with samba account in database\n");
			goto failed;
		}
	};


	# list workstations
	if ( $opt_list && defined($opt_workstation) ) {

		my $no_workstation = $opt_workstation;
		$opt_workstation = $opt_workstation || "*";
		my $USER_FILTER = sprintf("(&(objectclass=%s)(uid=%s))", $opt_smbacct ? $SMB_OC : "posixAccount", $opt_workstation);
		
		if ( ldap_search($ldap, $SUFFIX_MACHINES, $USER_FILTER) ) {
			debug(5,"list samba workstations: [$opt_workstation]\n");
			if (!ldap_list_smbacct($ldap, $opt_workstation,"workstation")) {
				goto failed;
			}
		} elsif ( $no_workstation eq "") { 
			debug(0,"no workstations found in database\n");
			goto done;
		} else {
			debug(0,"no workstation [$opt_workstation] with samba account in database\n");
			goto failed;
		}
	};


	# list groups 
	if ( $opt_list && defined($opt_group) ) {

		my $nogroup = $opt_group;
		$opt_group = $opt_group || "*";
		my $GROUP_FILTER = sprintf("(&(objectclass=%s)(cn=%s))", "posixGroup", $opt_group);

		if ( ldap_search($ldap, $SUFFIX_GROUPS, $GROUP_FILTER) ) {
			debug(5,"list members of group: [$opt_group]\n");
			ldap_list_group($ldap, $opt_group );
		} elsif ( $nogroup eq "") {
			debug(0,"no groups found in database\n");
			goto done;
		} else {
			debug(0,"no group [$opt_group] in database\n");
			goto failed;
		}
	}


	# delete user or workstation
	if ( $opt_delete && ($opt_user||$opt_workstation) ) {
#	if ( defined($opt_delete) && $opt_delete eq "" && ($opt_user||$opt_workstation) ) {

		my $name   = $opt_user || $opt_workstation;
		my $type   = sprintf("%s", $opt_user ? "user" : "workstation");
		my $suffix = sprintf("%s", $opt_user ? $SUFFIX_USERS : $SUFFIX_MACHINES);

		my $filter = sprintf "(&(|(objectclass=%s)(objectclass=%s))(uid=%s))", 
			"posixAccount", $SMB_OC, $name;

		my $num_result = ldap_search($ldap, $suffix, $filter);

		if ($num_result == 0) {
			debug(0,"no such $type [$name] to delete\n");
			goto failed;
		}

		if ($num_result > 1) {
			debug(0,("more then one to delete. exiting\n"));
			goto failed;
		}

		debug(0,"deleting $type: [$name]\n");
		if (!ldap_delete_internal($ldap, $name, $type, $entry->dn)) {
			goto failed;
		}
		goto done;
	};

	# add group
	if ( $opt_add && $opt_group ) {
		my $GROUP_FILTER = sprintf("(&(objectclass=%s)(cn=%s))", "posixGroup", $opt_group);
		my $num_result = ldap_search($ldap, $SUFFIX_GROUPS, $GROUP_FILTER);
		my $ridnumber;
		if ($num_result == 1) {
			if ( !$opt_rid ) {
				$ridnumber = $entry->get_value( 'gidNumber');
			} else {
				$ridnumber = $opt_rid;
			}
# TODO:			$ridnumber="" if (rid_is_used($ridnumber));
			if ( $opt_smbacct and $SAMBA_30 ) {
				debug(0,"adding ntgroup-mapping for unix-group: [$opt_group]\n");
				ldap_ntgroup_add($opt_group, $opt_comment, $ridnumber);
				goto done;
			}
			debug(0,"group [$opt_group] already exists. exiting.\n");
			goto failed;
		} 
		if ($num_result == 0) {
			if (!$opt_gid) { 
				$opt_gid = ldap_gid($ldap);
			} 
			if ( !$opt_gid ) {
				debug(0,"please define gidNumber with --gid=gidNumber\n");
				goto failed;
			} 
			# FIXME: check if gid is already used!
			debug(0,"adding group: [$opt_group] (with gidNumber: $opt_gid)\n");
			if (!ldap_posixgroup_add($ldap, $opt_group, $opt_gid)) {
				goto failed;
			}
			if ( !$opt_rid ) {
				$ridnumber = $opt_gid;
			} else {
				$ridnumber = $opt_rid;
			}
# TODO:			$ridnumber="" if (rid_is_used($ridnumber));
			if ( $opt_smbacct and $SAMBA_30 ) {
				debug(0,"adding ntgroup: [$opt_group]\n");
				# we just added the POSIX account -
				# make sure it is visible for ldap_ntgroup_add:
				refresh_nscd();
				ldap_ntgroup_add($opt_group, $opt_comment, $ridnumber);
			}
		}
	};


	# delete groupmapping
	if ( $opt_delete && $opt_group && $opt_smbacct && $SAMBA_30 ) {
		ldap_ntgroup_delete($opt_group);
	}


	# delete group
	if ( $opt_delete && $opt_group && !$opt_smbacct ) {
		my $GROUP_FILTER="(&(objectclass=posixGroup)(cn=$opt_group))";
		if ( ldap_search($ldap, $SUFFIX_GROUPS, $GROUP_FILTER) ) {
			debug(0,"deleting group: [$opt_group]\n");
			if (!ldap_delete_internal($ldap, $opt_group, "group", $entry->dn)) {
				goto failed;
			}
			goto done;
		} else {
			debug(0,"no such group [$opt_group] to delete. exiting.\n");
			goto failed;
		}
	};


	# add user to group
	if ( $opt_join && $opt_user && $opt_group ) {
		if (!join_or_remove_requisites($ldap, $opt_user, $opt_group)) {
			goto failed;
		}
		debug(0,"adding user [$opt_user] to group [$opt_group]\n");
		if (!ldap_user_group_member($ldap, $opt_user, $opt_group, "join")) {
			goto failed;
		}
	};


	# remove user from group
	if ( $opt_remove && $opt_user && $opt_group ) {
		if (!join_or_remove_requisites($ldap, $opt_user, $opt_group)) {
			goto failed;
		}
		debug(0,"removing user [$opt_user] from group [$opt_group]\n");
		if (!ldap_user_group_member($ldap, $opt_user, $opt_group, "remove")) {
			goto failed;
		}
	}


 done:
 	if ($opt_add || $opt_delete || $opt_modify || $opt_join || $opt_remove) {
		refresh_nscd();
	}
	ldap_unbind($ldap);
	close_logging();
	exit 0;

 failed:
	ldap_unbind($ldap);
	close_logging();
	exit -1;
}

main();

###############
# subroutines #
###############

sub get_rdn ($) {

	my $dn = shift;

	# escape "\,"
	# ads is escaping "," with a backslash (not RFC-conform), openldap converts "," into ascii "\2C")
	$dn =~ s/\\,/sumitstring/g;
	my ($rdn,@tmp) = split(/,/,$dn) ;
	$rdn =~ s/sumitstring/\\,/g;

	return $rdn;
}


sub naming_attribute_needs_update ($@) {

	my $dn = shift;
	my @changes = shift;

	my $rdn = get_rdn($dn);
	my $naming_attr = $rdn;
	$naming_attr =~ s/=.*//;

	foreach my $entry (@changes) {
		next if (!$entry);
		debug(10,"parsing entry: $entry\n");
		if ($naming_attr =~ /^$entry$/i) {
			debug(10,"found naming attribute to change: $entry\n");
			return $entry;
		}
	}
	
	return undef;
}

sub ldap_modrdn ($$$) {

	my $hd = shift;
	my $dn = shift;
	my $newrdn = shift;

	$newrdn = sprintf("%s=%s", "uid", $newrdn);

	my $tmp = get_rdn($dn);
	if ($tmp =~ /^$newrdn$/i) {
		debug(10,"FIXME: nothing to do, no change on the rdn-attribute\n");
		return True;
	}

	$mesg = $hd->moddn ( $dn, deleteoldrn => 1, newrdn => $newrdn);

	# was there an error?
	if ($mesg->code) {
		ldap_status("modrdn", $mesg, 3);
		return False;
	} 
	debug(5,"modified rdn of dn [$dn].\n");
	return True;
}


sub join_or_remove_requisites ($$$) {

	my $hd = shift;
	my $user = shift;
	my $group = shift;

	my $user_filter = sprintf("(&(objectclass=%s)(uid=%s))", "posixAccount", $user); 
	my $group_filter = sprintf("(&(objectclass=%s)(cn=%s))", "posixGroup", $group);

	# check if user is there
	if ( ldap_search($hd, $SUFFIX_USERS, $user_filter) ) {
		debug(5,"ok. user [$user] exists in ldap.\n");
	} elsif ( defined(getpwnam($user)) ) {
		debug(5,"ok. user [$user] exists somewhere else in the name service switch.\n");
	} else {
		debug(0,"no such user [$user]. exiting.\n");
		return False;
	}

	# check if group is there
	my $num_group = ldap_search($hd, $SUFFIX_GROUPS, $group_filter);
	if ($num_group > 1) {
		debug(0,"group [$group] exists more then once.\n");
		return False;
	}

	if ($num_group < 1) {
		debug(0,"no such group [$group]. exiting.\n");
		return False;
	}
	
	debug(5,"ok. group [$group] exists\n");
	return True;
}

sub ldap_list_smbacct {

	my $hd = shift;
	my $acct = shift || "*";
	my $type = shift;

	my $base;
	if ( $type =~ /user/i ) {
		$base = $SUFFIX_USERS;
	} elsif ( $type =~ /workstation|machine/ ) {
		$base = $SUFFIX_MACHINES;
	}
	
	$mesg = $hd->search(
		base => "$base", 
		filter => "(&(objectclass=$SMB_OC)(uid=$acct))");
	
	# was there an error?
	if ($mesg->code) {
		ldap_status("listing samba account", $mesg, 3);
		return False;
	}

	foreach my $entry ($mesg->all_entries) {

		my $uid 		= $entry->get_value( 'uid' );
		my $cn 			= $entry->get_value( 'cn' ) || "";
		my $uidNumber 		= $entry->get_value( 'uidNumber' ) || "";

		if ( $opt_raw ) {

			$entry->dump;
			next;

		} elsif ( $opt_verbose ) {

			my $acct		= $entry->get_value( 'sambaAcctFlags' ) || 
						  $entry->get_value( 'acctFlags' ) || "";
			my $description		= $entry->get_value( 'description' ) || "";
			my $displayName		= $entry->get_value( 'displayName' ) || "";
			my $domain		= $entry->get_value( 'sambaDomain' ) || 
						  $entry->get_value( 'domain' ) || "";
			my $gidNumber 		= $entry->get_value( 'gidNumber' ) || "";
			my $homeDirectory	= $entry->get_value( 'sambaHomeDirectory' ) || 
						  $entry->get_value( 'homeDirectory' );
			my $homeDrive 		= $entry->get_value( 'sambaHomeDrive' ) || 
						  $entry->get_value( 'homeDrive') || "";
			my $primaryGroupID	= $entry->get_value( 'primaryGroupID' );
			my $primaryGroupSID	= $entry->get_value( 'sambaPrimaryGroupSID' );
			my $profilePath		= $entry->get_value( 'sambaProfilePath' ) || 
						  $entry->get_value( 'profilePath' ) || "";
			my $rid 		= $entry->get_value( 'rid' );
			my $scriptPath 		= $entry->get_value( 'sambaScriptPath' ) || 
						  $entry->get_value( 'scriptPath' ) || "";
			my $sid 		= $entry->get_value( 'sambaSID' );
			my $wks			= $entry->get_value( 'sambaUserWorkstations' ) || 
						  $entry->get_value( 'userworkstations' ) || "";
			from_to($description, "utf-8", "iso-8859-1" );
	
			print "Unix username:\t\t$uid\n";
			print "NT username:\t\t$displayName\n";
			print "Account Flags:\t\t$acct\n";
			print "user ID/Group:\t\t$uidNumber / $gidNumber\n";
			if ( $SAMBA_22 ) { 
				print "user RID:\t$rid\n";
				print "user GRID:\t$primaryGroupID\n";
				my $ret = check_rid_allocation($uidNumber,$gidNumber,$rid,$primaryGroupID); 
				if ($ret) {
					debug(0,"$ret\n");
				}
			} elsif ( $SAMBA_30 ) {
				print "User SID:\t\t$sid\n";
				print "User Primary Group SID:\t$primaryGroupSID\n";
			}
			print "Full Name:\t\t$cn\n";
			print "Home Directory:\t\t$homeDirectory\n" if ($homeDirectory);
			print "HomeDir Drive:\t\t$homeDrive\n";
			print "Logon Script:\t\t$scriptPath\n";
			print "Profile Path:\t\t$profilePath\n";
			print "Domain:\t\t$domain\n" if ($domain);
			print "Workstations:\t\t$wks\n" if ($wks);
			print "Account Desc.:\t\t$description\n" if ($description);
			print "\n";
			print "------------------------------------\n" if ($mesg->entries > 1);
	
		} else {
	
			print "$uid:$uidNumber:$cn\n";
	
		}
	}
	return True;
}


sub ldap_list_group {

	my $hd = shift;
	my $group = shift || "*";

	$mesg = $hd->search (
		base => "$SUFFIX_GROUPS", 
		filter => "(&(objectclass=posixGroup)(cn=$group))"
	);

	# was there an error?
	if ($mesg->code) {
		ldap_status("listing group members", $mesg, 3);
	}

	foreach my $entry ($mesg->all_entries) {
	
		my $cn 		= $entry->get_value( 'cn' );
		my $gid 	= $entry->get_value( 'gidNumber' );
		my @memberUid 	= $entry->get_value( 'memberUid' );
		my $description = $entry->get_value( 'description' ) || "";
		my $sid 	= $entry->get_value( 'sambaSid' ) || "";
		my $nt_name 	= $entry->get_value( 'displayName' ) || "";
		my $group_type 	= $entry->get_value( 'sambaGroupType' ) || "";
		if (! $HAVE_UTF8) {
			from_to($cn, "utf-8", "iso-8859-1" );
			from_to($nt_name, "utf-8", "iso-8859-1" );
			from_to(@memberUid, "utf-8", "iso-8859-1" );
			from_to($description, "utf-8", "iso-8859-1" );
		}
		if ( $opt_verbose ) {
			print "Groupname:\t$cn\n";
			print "NT-Groupname:\t$nt_name\n" if ($nt_name);
			print "Group ID:\t$gid\n";
			print "Group SID:\t$sid\n" if ($sid);
			print "Group Type:\t$group_type\n" if ($group_type);	#FIXME
			printf "Groupmembers:\t%s\n", join(",",@memberUid) if (@memberUid);
			print "Description:\t$description\n" if ($description);
			print "\n";
			print "--------------------------------\n" if ($mesg->entries > 1);
		} elsif ( $opt_raw ) {
			$entry->dump;
		} else {
			printf "$cn:x:$gid:%s\n", join(",",@memberUid);
		}
	}
}


sub ldap_list_user ($$) {

	my $hd = shift;
	my $user = shift || "*";
	
	$mesg = $hd->search (
		base => "$SUFFIX_USERS", 
		filter => "(&(objectclass=posixAccount)(cn=$user))"
	);

	# was there an error?
	if ($mesg->code) {
		ldap_status("listing users", $mesg, 3);
		return False;
	} 

	foreach my $entry ($mesg->all_entries) {

		my $cn 		= $entry->get_value( 'cn' );
		my $uid 	= $entry->get_value( 'uid' );
		my $uidNumber 	= $entry->get_value( 'uidNumber' );
		my $gid 	= $entry->get_value( 'gidNumber' );
		my $home 	= $entry->get_value( 'homeDirectory' );
		my $shell 	= $entry->get_value( 'loginShell' );
		my $description = $entry->get_value( 'description' ) || "";
		my $gecos	= $entry->get_value( 'gecos' ) || "";
		if (! $HAVE_UTF8) {
			from_to($cn, "utf-8", "iso-8859-1" );
			from_to($description, "utf-8", "iso-8859-1" );
		}
		if ( $opt_verbose ) {
			print "Username:\t$cn\n";
			print "User ID:\t$uid\n";
			print "Description:\t$description\n" if ($description);
			print "Group ID:\t$gid\n";
			print "Home Directory: $home\n";
			print "Gecos:\t\t$gecos\n";
			print "Shell:\t\t$shell\n";
			print "\n";
			print "--------------------------------\n" if ($mesg->entries > 1);
		} elsif ( $opt_raw ) {
			$entry->dump;
		} else {
			printf("%s:x:$uidNumber:$gid:$gecos:$home:$shell\n", $uid ? $uid : $cn);
		}
	}
	return True;
}


sub ldap_posixuser_add ($$$$) {

	my $hd = shift;
	my $uid = shift;
	my $uidNumber = shift;
	my $gidNumber = shift || $PRIMARYGIDNUMBER;

	my $userPassword = undef;

	my $home	= $opt_homedir 	|| "$HOMEDIR_ROOT/$ascii_user";
	my $shell	= $opt_loginShell || "/bin/bash";
	my $cn 		= $opt_comment	|| $uid;

	# check passwd
	my $salt =  pack("C2",(int(rand 26)+65),(int(rand 26)+65));
	if ( $USE_TRIVIAL_PWD ) {
		$opt_passwd = $opt_user;
		$userPassword =	crypt "$opt_passwd",$salt;
		$userPassword =	"{crypt}".$userPassword;
	} elsif ( defined($opt_passwd) ) {
		$userPassword =	crypt "$opt_passwd",$salt;
		$userPassword =	"{crypt}".$userPassword;
	} else {
		$userPassword = "";
	}

	# some more defaults
	my $dn		=	sprintf("uid=%s,%s", $uid, $SUFFIX_USERS);

	# create a new object
	$entry = Net::LDAP::Entry->new;

	$entry->dn ( "$dn" );
	$entry->add ( objectclass => [ $STRUCT_OC_POSIX_ACCOUNT, 'posixAccount' ] );
	$entry->add (	'cn'            => $cn,
			'uid'           => $uid,
			'uidNumber'     => $uidNumber,
			'gidNumber'     => $gidNumber,
			'homeDirectory' => $home,
			'loginShell'    => $shell,
#			'gecos'         => $GECOS, 
	);
	if ($STRUCT_OC_POSIX_ACCOUNT eq "inetOrgPerson") {
		$entry->add (	'sn'	=> "dummy");
	}

	$entry->add (	'userPassword'  => "$userPassword" ) if ( $userPassword );

	if ( ldap_update($hd, $entry, True, "adding user") ) {
		debug(5,"added user [$uid] posixAccount.\n");
	} else {
		return False;
	}

	# create home directory and copy skeleton files
	if ( $opt_makehomedir ) {
		if (! -d "$home") {
			debug( 5, "Creating home directory <".$home."> of user [$opt_user]\n");
			mkdir( "$home") or die "Can't create directory: $!\n";
			chown( "$uidNumber", "$gidNumber", "$home");
		}
		$opt_skeldir = $opt_skeldir || $SKELETONDIR;
		debug( 5, "Copying skeleton files from <".$opt_skeldir."> to <".$home.">\n");
		system( "/usr/bin/find \"$opt_skeldir\" -mindepth 1 -maxdepth 1 -print0 | /usr/bin/xargs -0 /bin/cp -a --target-directory=\"$home\"");
		debug( 5, "Change owner in <".$home."> to <".$uidNumber.">:<".$gidNumber.">\n");
		system( "/usr/bin/find \"$home\" -mindepth 1 -maxdepth 1 -print0 | /usr/bin/xargs -0 /bin/chown \"$uidNumber\":\"$gidNumber\"");
	}
	return True;
}


sub ldap_smbuser_add ($) {

	my $uid = shift;	

	# create sambaAccount of user
	if ( $opt_smbacct && ($opt_mode eq "local") ) {

		$opt_passwd = $opt_passwd || "";
		debug(5,"Creating samba account of user [$uid]\n");
		if ($DEBUG_PASSWORD) {
			debugadd(5,"with password <$opt_passwd>.\n");
		} else {
			debugadd(5,"with password xxx (not logged).\n");
		}
	
		# although displayName is a UTF-8 string (1.3.6.1.4.1.1466.115.121.1.15) 
		# pdbedit is not encoding it correctly. FIXME: tell samba-team 
		my $gecos = (getpwnam($uid))[6];
		my $ascii_displayName = umlaut2ascii($gecos);
	
		if ( $SAMBA_22 ) {
			my $pdbedit_opts = "-a -u -b -f '$ascii_displayName'";
			system( "/bin/echo -e \"$opt_passwd\\n$opt_passwd\" | $progs{pdbedit} $pdbedit_opts \"$uid\"");
		
		} elsif ( $SAMBA_30 ) {
			# FIXME: patch pdbedit!
#			my $pdbedit_opts = "-a -u -b ldapsam://$LDAP_SERVER:$LDAP_PORT";
#			my $smbpasswd_opts = "-a -e -s";
#			system( "/bin/echo -e \"$opt_passwd\\n$opt_passwd\" | $progs{smbpasswd} $smbpasswd_opts \"$uid\"");
			my $pdbedit_opts = "-a -f '$ascii_displayName'";
			system( "$progs{pdbedit} -d 0 $pdbedit_opts \"$uid\" 2>&1 > /dev/null");
		}
		# inform if there was an rc != 0
		if ($?) {
			debug(0,"Creating samba account of user [$uid] failed.\n");
			return False;
		}
	}
	return True;
}


sub ldap_usermodify ($$) {

	my $hd = shift;
	my $user = shift;

	my ($homeDirectory,$userPassword,$cn);
	my %changes = ();
	my %sambachanges = ();

	if ( $opt_homedir ) {
		$changes{'homeDirectory'} = $opt_homedir;
		$sambachanges{'-h'} = "";
	}
	if ( $opt_gid ) {
		$changes{'gidNumber'} = $opt_gid;
		$sambachanges{'-g'} = "";
	}
	if ( $opt_uid ) {
		$changes{'uidNumber'} = $opt_uid;
		$sambachanges{'-u'} = "";
	}
	if ( $opt_username ) {
		$changes{'uid'} = $opt_username;
	}
	if ( $opt_loginShell ) {
		$changes{'loginShell'} = $opt_loginShell;
	}
	if ( $opt_passwd ) {
		my $salt =  pack("C2",(int(rand 26)+65),(int(rand 26)+65));
		$changes{'userPassword'} = crypt "$opt_passwd",$salt;
		$changes{'userPassword'} = "{crypt}".$changes{'userPassword'};
	}
	if ( $opt_comment ) {
		$changes{'cn'} = $opt_comment;
		$sambachanges{'-f'} = "\"$opt_comment\"";
	}
	my $dn = $entry->dn;

	my $attr = naming_attribute_needs_update($dn, %changes);
	if ($attr) {
		debug(3,"have to change naming attribute for [$dn]\n");
		if (!ldap_modrdn($hd, $dn, $opt_username)) {
			return False;
		}
		my $num_result = ldap_search($hd, $SUFFIX_USERS, "(&(objectclass=posixAccount)(uid=$opt_username))");
		if ($num_result != 1) {
			debug(0,("could not find renamed account. DIT corrupt?\n"));
			return False;
		} else {
			# reset $dn
			$dn = $entry->dn;
		}
	}

	$mesg = $hd->modify( "$dn", replace => \%changes);
	# was there an error?
	if ($mesg->code) {
		ldap_status("modifying user", $mesg, 3);
	} else {
		debug(5,"modified posixAccount of user [$opt_user].\n");
	}

	# apply sambaAccount changes
	if ( $opt_smbacct && ($opt_mode eq "local") ) {
		debug( 5,"modifying samba account of user [$opt_user].\n");
		if ( $opt_passwd ) {
			if ($DEBUG_PASSWORD) {
				debugadd(5,"with password [opt_passwd].\n");
			}
			system( "/bin/echo -e \"$opt_passwd\\n$opt_passwd\" | $progs{smbpasswd} -s $opt_user --debug 0 2>&1 >/dev/null");
		}
		my ($opt,$arg);
		my $pdbeditopts = "-u \"$opt_user\"";
		while (($opt, $arg) = each %sambachanges) {
			debug(3,"$opt, $arg");
			$pdbeditopts .= " ".$opt." ".$arg;
		}
		debug(3,"$progs{pdbedit} $pdbeditopts");
		system( "$progs{pdbedit} $pdbeditopts -d0 2>&1 >/dev/null");
		# inform if there was an rc != 0
		if ($?) {
			debug(0,"modifying samba account of user [$opt_user] failed.\n");
		}
	}

	return True;
}


sub ldap_posixwks_add ($$) {

	my $hd = shift;
	my $uid = shift;
	my $uidNumber = shift;

	my $home		= $opt_homedir || "/dev/null";
	my $shell		= $opt_loginShell || "/bin/false";
	my $gidNumber		= $PRIMARYGIDNUMBER_WKS;
	my $dn			= sprintf("uid=%s,%s", $uid, $SUFFIX_MACHINES);
	my $cn 			= $opt_comment || "Windows Workstation ".uc($uid);

	# create a new object
	$entry = Net::LDAP::Entry->new;

	$entry->dn ( "$dn" );
	$entry->add ( objectclass => [ $STRUCT_OC_POSIX_ACCOUNT, 'posixAccount' ] );
	$entry->add (	'cn'            => $cn,
			'uid'           => $uid,
			'uidNumber'     => $uidNumber,
			'gidNumber'     => $gidNumber,
			'homeDirectory' => $home,
			'loginShell'    => $shell,
#			'gecos'         => $GECOS,
	);
	if ($STRUCT_OC_POSIX_ACCOUNT eq "inetOrgPerson") {
		$entry->add (	'sn'	=> "dummy");
	}

	# update
	if (!ldap_update($hd, $entry, True, "adding machine account") ) {
		return False;
	}

	debug(5,"added machine account [$uid]\n");
	return True;
}


sub ldap_smbwks_add {

	my $wks = shift;

	# create sambaAccount of machine
	if ( $opt_smbacct && ($opt_mode eq "local") ) {
		debug(10,"Creating samba account of machine [$wks].\n");
		system( "$progs{smbpasswd} -a -m '$wks'");
		if ($? != 0) {
			debug(0,"Creating samba account of machine [$wks] failed.\n");
			return False;
		}
	}
	return True;
}

sub ldap_delete_internal ($$$$) {

	my $hd = shift;
	my $name = shift;
	my $type = shift;
	my $dn = shift;
	
	if ($type !~ /user|group|workstation/ ) {
		debug(0,"unkown type ($type). cannot delete\n");
		return False;
	}

	my $mesg = $hd->delete( $dn );
	# was there an error?
	if ($mesg->code) {
		ldap_status("deleting $type", $mesg, 3);
	} else {
		debug(3,"deleted $type $name ($dn)\n");
	}
	return True;
}

sub ldap_posixgroup_add ($$) {

	my $hd 		= shift;
	my $group	= shift;
	my $gidNumber 	= shift;

	my $dn 		= sprintf("cn=%s,%s", $group, $SUFFIX_GROUPS);

	# create a new object
	$entry = Net::LDAP::Entry->new;

	$entry->dn ( "$dn" );
	$entry->add ( objectclass => [ 'posixGroup', 'top', $STRUCT_OC_POSIX_GROUP ] );
	$entry->add (	'cn'            => "$group",
			'gidNumber'     => "$gidNumber");

	# update
	if (!ldap_update($hd, $entry, True, "adding posixgroup")) {
		return False;
	}
	debug(10,"added posix group [$group] (with gidNumber: $gidNumber)\n");
	return True;
}


sub ldap_ntgroup_add {

	my $ntgroup = shift;
	my $comment = shift;
	my $rid = shift;
	my $rid_opt = $rid?"rid=\"$rid\"":"";
	my $comment_opt = $comment?"comment=\"$comment\"":"";
	my $ntgroup_type = "domain";
	my $ret = system("$progs{net} groupmap add $rid_opt unixgroup=\"$ntgroup\" type=$ntgroup_type ntgroup=\"$ntgroup\" $comment_opt -d 0 2>&1 >/dev/null");
	return $ret;
}


sub ldap_ntgroup_delete {

	my $ntgroup = shift;
	my $ret = system("$progs{net} groupmap delete ntgroup=$ntgroup \
		-d 0 2>&1 >/dev/null");
	return $ret;
}


sub ldap_user_group_member ($$$$) {

	my $hd = shift;
	my $user = shift;
	my $group = shift;
	my $mode = shift;

	my $user_is_member = False;
	my @members;
	my $GROUP_FILTER = "(&(objectclass=posixGroup)(cn=$group))";
	my $mesg = ldap_search($hd, "$SUFFIX_GROUPS", "$GROUP_FILTER");
	my $mem_ref = $entry->get_value( 'memberUid', asref => 1 );
	if ($mem_ref) {
		@members = @$mem_ref;
		debug(5,"group [$group] currently has these members: [", join(", ", @members), "]\n");
		foreach my $member (@members) {
			if ($member eq $user ) { $user_is_member = True; }
		}
	}

	if ($mode eq "join") {
	
		if ($user_is_member) {
			debug(0,"doing nothing. [$user] is already a member of [$group]\n");
			return True;
		}
	
		push (@members,$user);
		$entry->add( 'memberUid' => "$user");
	}

	if ($mode eq "remove") {
	
		if (!$user_is_member) {
			debug(0,"doing nothing. [$user] is not a member of [$group]\n");
			return True;
		}

		$entry->delete( 'memberUid' => ["$user"] );
	}
	
	debug(5,"group [$group] now has these members: [", join(", ", @members), "]\n");

	# update
	if (!ldap_update($hd, $entry, True, "ldap_user_group_member modify") ) {
		return False;
	}
	
	if ($mode eq "join") {
		debug(10,"added user [$user] to posix group [$group]\n");
	} elsif ($mode eq "remove") {
		debug(10,"removed user [$user] from posix group [$group]\n");
	}
	
	return True;
}

#######################
# basic ldap operations
#######################

sub ldap_search {

	debug(5,"################### query LDAP ######################\n");

	my $hd		= shift;
	my $base 	= shift || $SUFFIX;
	my $filter 	= shift || "(objectclass=*)";
	my $attrs 	= shift || "*";
	my $scope 	= shift || "sub";

	# construct the query
	my $mesg = $hd->search (
		base    =>      "$base",
		filter  =>      "$filter",
		attrs   =>      [ $attrs, '+' ],
		scope   =>      "$scope"
	);

	# was there an error?
	if ($mesg->code) {
		ldap_status("searching", $mesg, 3);
		debug(3,"\tsearch with:\n");
		debugadd(3,"\t\tbase\t\"$base\"\n");
		debugadd(3,"\t\tfilter\t\"$filter\"\n");
		debugadd(3,"\t\tattrs\t\"$attrs\"\n");
		debugadd(3,"\t\tscope\t\"$scope\"\n");
		debugadd(3,"\tgave an error\n");
		die; #sure?
	} else {
		debugadd(5,"\tsearch with:\n");
		debugadd(5,"\t\tbase\t\"$base\"\n");
		debugadd(5,"\t\tfilter\t\"$filter\"\n");
		debugadd(5,"\t\tattrs\t\"$attrs\"\n");
		debugadd(5,"\t\tscope\t\"$scope\"\n");
		debugadd(5,"\tgave no error\n");
	}

	# report back
	$entry = $mesg->entry;
	if ( ! $entry ) {
		debugadd(5,"no entry found with filter \"$filter\"\n");
		debugadd(5,"################ finished LDAP query ################\n");
		return 0;
	} else {
		debugadd(5,"found ".$mesg->count." matching entrie(s)\n");
		for (my $i = 0; $i < $mesg->count; $i++) {
			debugadd(5,"found entry with dn:\t\"".$mesg->entry($i)->dn."\"\n");
		}
		debugadd(5,"################ finished LDAP query ################\n");
		return $mesg->count;
	}
}


sub ldap_bind {

	my $mode = shift || "qualified";
	my $mesg;
	my $status = "";
	my $ldap;
	my $uri;

	if ($LDAP_SSL eq "yes") {
		$uri = sprintf("%s://%s", "ldaps", $LDAP_SERVER);
		if (! $HAVE_NET_LDAPS) {
			goto no_ssl_or_tls;
		}
	} else {
		$uri = sprintf("%s://%s", "ldap", $LDAP_SERVER);
	}

	if ($USE_LDAPI && $HAVE_LDAPI) {
		debug(10,"opening LDAPI socket ($LDAPI_SOCKET)\n");
		my $ldapi_url = sprintf("ldapi://%s", join("%2f", split(/\//, $LDAPI_SOCKET)));
		$ldap = Net::LDAP->new( $ldapi_url ) || die "could not open LDAPI socket ($LDAPI_SOCKET): $@";
	} else {
		debug(10,"opening socket to server: \'$LDAP_SERVER\', port: \'$LDAP_PORT\'\n");
		$ldap = Net::LDAP->new( $uri, port => $LDAP_PORT, version => 3 ) || 
			Net::LDAP->new( $uri, port => $LDAP_PORT, version => 2 );
		if (!$ldap) {
			debug(0,"could not open TCP/IP socket to $LDAP_SERVER: $@\n");
			return undef;
		}
	}

	if ($START_TLS) {

		if (! $HAVE_NET_LDAPS) {
			goto no_ssl_or_tls;
		}

		$mesg = $ldap->start_tls( verify => 'none' );
		if ($mesg->code) {
			ldap_status("starting tls", $mesg, 3); 
			die;
		} else {
			debugadd(10,"START_TLS was successfull.\n");
		}
	} 

	if ($mode =~ /anonymous/i) {
		$mesg = $ldap->bind();
		$status = "binding anonymously";
	} else {
		$mesg = $ldap->bind( "$ADMIN_DN", password => $ADMIN_PW );
		if ($DEBUG_PASSWORD) {
			$status = "binding as \"$ADMIN_DN\" with password \"$ADMIN_PW\"";
		} else {
			$status = "binding as \"$ADMIN_DN\" with password xxx (not logged)";
		}
	}
	if ($mesg->code) {
		ldap_status($status, $mesg, -1); 
		die;
	} else {
		debugadd(10,"$status was successful.\n");
	}
	return $ldap;

 no_ssl_or_tls:
	debug(0,"perl-ldap-ssl (Net::LDAPS) is not installed\n");
	debugadd(0,"cannot bind to: $uri\n");
	return undef;
} 

sub ldap_anon_bind() {
	return ldap_bind("anonymous");
}

sub ldap_unbind ($) {

	my $hd = shift;

	my $status = "unbinding";
	$mesg = $hd->unbind;
	if ($mesg->code) {
		ldap_status($status, $mesg, 3); 
		die;
	} else {
		debug(10,"$status was successful.\n");
	}
}


sub ldap_status {

	my ($from, $mesg, $tmp) = @_;
	$tmp = $tmp || "5";
	debug($tmp,"\twhile:\t\t$from\n");
	debugadd($tmp,"\terror:\t\t".$mesg->error()."\n");
	debugadd($tmp,"\treturn code:\t".$mesg->code."\n");
	debugadd($tmp,"\tmessage:\t".ldap_error_name($mesg->code).": \n");
	debugadd($tmp,"\t\t\t".ldap_error_text($mesg->code));
	debugadd($tmp,"\tmessageID:\t".$mesg->mesg_id."\n");
	debugadd($tmp,"\tdn:\t\t".$mesg->dn."\n");
}

sub ldap_update ($$$$) {

	my $hd 		= shift;
	my $entry	= shift;
	my $critic 	= shift || True; # critical update ?
	my $status_mesg = shift || "updating (modifying)";

	my $dn = $entry->dn;
	debug(5,"updating dn:\t\"$dn\"\n");

	my $mesg_update = $entry->update( $hd );

	if ($mesg_update->code) {
		ldap_status("$status_mesg", $mesg_update, 3);
		if ($critic) {
			# now we really die during an update
			debugadd(3,"we die. error during an update-attempt\n");
			die;
		} else {
			# now we do not die
			debugadd(5,"error, but we do not die\n");
			return False;
		}

	} 
	
	ldap_status("$status_mesg", $mesg_update);
	debugadd(5,"update was successful.\n");
	return True;
}


#######################
# ldap uid/gid handling
#######################

sub valid_id_range ($) {

	my $range = shift;
	
	if ($range =~ /^\d+-\d+$/ ) {
		return True;
	}
	
	debug(0,"invalid id-range\n");
	return False;
}

sub get_maxid_val ($) {

	my $entry = shift || goto failed;
	my $val = $entry->get_value( 'description' ) || goto failed;
	if ($val =~ /^\d+$/ ) {
		return $val;
	}
 failed:
	debug(0,"invalid maxid value\n");
	return undef;
}

sub check_maxid_watermark ($$) {

	my $current_id = shift;
	my $max_limit  = shift;
	if ( $current_id >= $max_limit ) {
		debug(0,"cannot add new accounts.\n");
		debugadd(0,"highest {u|g}id already reached ($current_id >= $max_limit). exiting\n");
		return False;
	}
	return True;
}

sub check_nss_uses_id ($$) {
	
	my $id = shift;
	my $id_type = shift;
	
	if ($id_type eq "user") {
		if (getpwuid($id)) {
			debug(0,"uid $id is already in use\n");
			return True;
		}
	} elsif ($id_type eq "group") {
		if (getgrgid($id)) {
			debug(0,"gid $id is already in use\n");
			return True;
		}
	}
	return False;
}

sub ldap_id ($$) {

	my $hd = shift;
	my $id = shift;
	
	my ($ldap_attr,$ID_RANGE,$ldap_uid_attr,$user_or_group,$oc,$NEW_MAXID);

	if (!$USE_MAXID) {
		return undef;
	}

	if ( $id =~ /uid/i ) {
		$ID_RANGE 	= $UID_RANGE;
		$ldap_uid_attr 	= "maxuid";
		$user_or_group 	= "user";
		$oc 		= "posixAccount";
		$ldap_attr 	= "uidNumber";
	} elsif ( $id =~ /gid/i ) {
		$ID_RANGE 	= $GID_RANGE;
		$ldap_uid_attr 	= "maxgid";
		$user_or_group 	= "group";
		$oc 		= "posixGroup";
		$ldap_attr 	= "gidNumber";
	} else {
		debug(3,"unknown id-type",$id,"\n");
		return undef;
	}

	if (!valid_id_range($ID_RANGE)) {
		return undef;
	}
	
	my ($MIN_ID,$MAX_ID) = split(/-/,$ID_RANGE);

	my $INIT_MAXID = $MIN_ID; 

	debug(5,"got order to generate a new $ldap_attr\n");

	# look for maxid in ldap
	my $num_result = ldap_search($hd, $SUFFIX, "(&(objectclass=top)(uid=$ldap_uid_attr))");
	if ($num_result > 1) {
		debug(0,"more then one MAXUID-object found. exisiting\n");
		return undef;
	}
	
	if ($num_result == 1) {

		# if it is there, get the value
		my $LDAP_MAXID = get_maxid_val($entry);
		if (!$LDAP_MAXID) {
			return undef;
		}

		# check maxid
		if (!check_maxid_watermark($LDAP_MAXID, $MAX_ID)) {
			return undef;
		}

		# increase
		$NEW_MAXID = $LDAP_MAXID + 1;

		debug(5,"MAXID ($id) is currently $LDAP_MAXID, next $user_or_group will have $NEW_MAXID\n");

		# store that entry
		my $maxid_entry = $entry;

		# double-check if no one uses this id
		my $found_id = 0;
		until ( $found_id ) {

			if ( ldap_search($hd, $SUFFIX, "(&(objectclass=$oc)($ldap_attr=$NEW_MAXID))") ||
			     check_nss_uses_id($NEW_MAXID, $user_or_group)) {
				debug(5,"bad. $ldap_attr $NEW_MAXID is already in use.\n");
				# possibly try to find the next
				++$NEW_MAXID;
				debug(5,"trying $NEW_MAXID now\n");

				# check maxid
				if (!check_maxid_watermark($LDAP_MAXID, $MAX_ID)) {
					return undef;
				}

			} else {

				# check maxid
				if (!check_maxid_watermark($LDAP_MAXID, $MAX_ID)) {
					return undef;
				}

				# and update
				debug(5,"writing updated MAXID ($id) $NEW_MAXID to ldap-tree\n");
				$entry = $maxid_entry;
				my $mesg = $maxid_entry->replace( 'description' => $NEW_MAXID );
				ldap_update($hd, $entry, True, "updating maxid ($id)");
				debug(5,"updated MAXID ($id)\n");

				# and use it
				if ( $id =~ /uid|gid/i ) {
					return $NEW_MAXID;
				}
				$found_id = 1;
			}
		}

		return undef;

	} 
	
	# if not there, initialize it:
	debug(0,"initialising maxid for $id\n");
	$INIT_MAXID = "$MIN_ID";

	# double-check if no one uses our uid
	my $found_id = 0;
	until ( $found_id ) {

		if ( ldap_search($hd, $SUFFIX, "(&(objectclass=$oc)($ldap_attr=$INIT_MAXID))") ||
		     check_nss_uses_id($INIT_MAXID, $user_or_group)) {

			# check maxid
			if (!check_maxid_watermark($INIT_MAXID, $MAX_ID)) {
				return undef;
			}

			debug(5,"bad. $ldap_attr $INIT_MAXID is already in use.\n");
			# possibly try to find the next
			++$INIT_MAXID;
			debugadd(5,"trying $INIT_MAXID for $id now\n");

		} else {

			# check maxid
			if (!check_maxid_watermark($INIT_MAXID, $MAX_ID)) {
				return undef;
			}

			debug(5,"writing updated INIT_MAXID $INIT_MAXID (for $id) to ldap-tree\n");
			$entry = Net::LDAP::Entry->new;
			my $dn = "uid=$ldap_uid_attr,$SUFFIX";
			debugadd(5,"++\tadding new object with:\tdn $dn\n");
			$entry->dn( "$dn" );
			$entry->add( 'objectclass' => [("top", "$STRUCT_OC_POSIX_ACCOUNT")]);
			$entry->add( 'description' => $INIT_MAXID);
			$entry->add( 'uid' => $ldap_uid_attr);

			if ($STRUCT_OC_POSIX_ACCOUNT eq "inetOrgPerson") {
				$entry->add( 'sn' => "dummy");
				$entry->add( 'cn' => "dummy");
			}

			ldap_update($hd, $entry, True, "adding maxid for $id");
			debugadd(5,"updated MAXID for $id\n");

			# and use it
			if ( $id =~ /uid|gid/i ) {
				return $INIT_MAXID;
			}

			$found_id = 1;
		}

	}
	return undef;
} 

sub ldap_uid ($) {
	my $hd = shift;
	return ldap_id($hd, "uid");
}

sub ldap_gid ($) {
	my $hd = shift;
	return ldap_id($hd, "gid");
}

sub ldap_init ($) {

	my $hd = shift;

	# FIXME: this is rubbish...
	debug(3,"initializing your ldap-tree, some data will be deleted!\n");

	my %ous = (
		$SUFFIX 	=> 0, 
		$SUFFIX_GROUPS	=> 0,
		$SUFFIX_USERS	=> 0,
		$SUFFIX_MACHINES => 0,
		$SUFFIX_IDMAP	=> 0);

	my $result;
	
	foreach my $ou (sort keys %ous) {
	
		next if (!$ou);
		
		next if ($ou =~ /^$SUFFIX$/);

		my $tmp = $ou;
		$tmp =~ s/^ou=//i;
		$tmp =~ s/,.*//i;
		debug(3,"adding: \t $ou\n");
		$result = $hd->add ( 	"$ou",
			attrs => [ 'objectClass' => 'organizationalUnit', 'ou' => "$tmp" ]);
		ldap_status("creating ous", $result, -1);
	}
}


sub print_smb_conf ($) {

	my $hd = shift;

	# print what we got
	print "\nRunning-mode:\t\t\t$opt_mode\n";
	printf "\nLDAP-mode:\t\t\tLDAP over %s\n", ($USE_LDAPI && $HAVE_LDAPI) ? "IPC ($LDAPI_SOCKET)" : "TCP/IP";
	print "\nIdentified Samba Version:\t$SAMBA_VERSION\n" if ($SAMBA_VERSION);
	print "\nAutodetected from [$SMB_CONF]:\n";
	print "\tldap admin dn:\t\t$ADMIN_DN\n";
	print "\tldap suffix:\t\t$SUFFIX\n";
	print "\tldap machine suffix:\t$SUFFIX_MACHINES\n" if ($SUFFIX_MACHINES);
	print "\tldap user suffix:\t$SUFFIX_USERS\n" if ($SUFFIX_USERS);
	print "\tldap group suffix:\t$SUFFIX_GROUPS\n" if ($SUFFIX_GROUPS);
	print "\tldap idmap suffix:\t$SUFFIX_IDMAP\n" if ($SUFFIX_IDMAP);
	print "\tldap server:\t\t$LDAP_SERVER\n";
	print "\tldap port:\t\t$LDAP_PORT\n";
	print "\tldap ssl:\t\t$LDAP_SSL\n";

	print "\nAutodetected from [$confs{'secrets.tdb'}]:\n";
	print "\tldap admin password:\t$ADMIN_PW\n";

	print "\nAutodetected binaries and configuration files:\n";
	foreach my $prog ( @progs ) {
		printf("\t%-23s %s\n", $prog, $progs{$prog});
	}
	foreach my $conf ( @confs ) {
		printf("\t%-23s %s\n", $conf, $confs{$conf});
	}

	print "\nwill use:\nldap group suffix:\t$SUFFIX_GROUPS\n" if ($SAMBA_22);

	if ( $USE_MAXID && $hd) {
		print "\nMAXID-settings stored in ldap:\n";
		if ( ldap_search($hd, "$SUFFIX","(&(objectclass=top)(uid=maxuid))") ) {
			my $MAXUID = get_maxid_val($entry);
			if (!$MAXUID) {
				return;
			}
			print "\tmaxUid:\t\t\t$MAXUID\n";
			if (!valid_id_range($UID_RANGE)) {
				return;
			}
			my ($MIN_UID,$MAX_UID) = split(/-/,$UID_RANGE);
			print "\tuid range:\t\t$UID_RANGE\n";
			print "\tyou can add:\t\t",$MAX_UID-$MAXUID, " user accounts\n";
		}
		if ( ldap_search($hd, "$SUFFIX","(&(objectclass=top)(uid=maxgid))") ) {
			my $MAXGID = get_maxid_val($entry);
			if (!$MAXGID) {
				return;
			}
			print "\tmaxGid:\t\t\t$MAXGID\n";
			if (!valid_id_range($GID_RANGE)) {
				return;
			}
			my ($MIN_GID,$MAX_GID) = split(/-/,$GID_RANGE);
			print "\tgid range:\t\t$GID_RANGE\n";
			print "\tyou can add:\t\t",$MAX_GID-$MAXGID, " group accounts\n";
		}
		print "\n";
	}
}


sub debug_internal {

	my $loglevel = shift;
	my $add = shift;
	my $msg = "@_";

	my ($package, $filename, $line, $subroutine) = caller(0);
	my ($p_package, $p_filename, $p_line, $p_subroutine) = caller(1);
	my ($pp_package, $pp_filename, $pp_line, $pp_subroutine) = caller(2);
	my $sub = $pp_subroutine || $p_subroutine || "";

	$sub =~ s/^.*:://g; 
	$filename =~ s/.*\///g;
	my $date = strftime "%b %e %H:%M:%S", localtime;
	my $prog = "$filename:$sub($line)";

	if ( $DEBUGLEVEL >= $loglevel ) {

		# write to STDOUT and with a timestamp to our logfile
		if ($USE_LOGFILE) {
			select(LOGFILE_HANDLE);
			$~ = ($add?"LOGFILE_HANDLE_ADD":"LOGFILE_HANDLE");
			write(LOGFILE_HANDLE);
		}

		if ($USE_SYSLOG) {
			syslog('info', $msg);
		}

		select(STDOUT);
		my $ofh = select(STDOUT);
		if ($DEBUGLEVEL == 0) {
			$~ = "DEBUG_STDOUT_0";
		} else {
			$~ = "DEBUG_STDOUT";
		}
		select($ofh);
		write();

	} elsif ( $loglevel <= 10) {
		if ($USE_LOGFILE) {
			# write to STDOUT and with a timestamp to our logfile
			select(LOGFILE_HANDLE);
			$~ = ($add?"LOGFILE_HANDLE_ADD":"LOGFILE_HANDLE");
			write(LOGFILE_HANDLE);
			select(STDOUT);
		}
		if ($USE_SYSLOG) {
			syslog('info', $msg);
		}
	}
	
format LOGFILE_HANDLE_ADD =
   @*
$msg
.
format LOGFILE_HANDLE =
[@<<<<<<<<<<<<<<] @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
$date, $prog
   @*
$msg
.
format DEBUG_STDOUT =
@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @*
$prog, $msg
.
format DEBUG_STDOUT_0 =
@*
$msg
.
}

sub debugadd {

	my $loglevel = shift;
	my @msg = @_;
	debug_internal($loglevel, 1, @msg);
}

sub debug {

	my $loglevel = shift;
	my @msg = @_;
	debug_internal($loglevel, 0, @msg);
}

sub umlaut2ascii {

	my $name = shift || "";

	# german characters
	$name =~ s/ä/ae/g ;
	$name =~ s/Ä/Ae/g ;
	$name =~ s/ü/ue/g ;
	$name =~ s/Ü/Ue/g ;
	$name =~ s/ö/oe/g ;
	$name =~ s/Ö/Oe/g ;
	$name =~ s/ß/ss/g ;

	return $name;

}


sub init_logging {

	if ($opt_quiet) {
		$DEBUGLEVEL = "-1";
	}
	if ($USE_SYSLOG) {
		openlog("ldapsmb", "", "LOG_USER");
		return;
	}
	return if (! $USE_LOGFILE);
	
	# open the logfile, or create it if not there
	# FIXME: what to do if it exists, but we can't write ??
	unless (-w $LOGFILE_NAME) {
		open (LOGFILE_HANDLE, ">$LOGFILE_NAME") || die "cannot create logfile: $!";
	}
	open (LOGFILE_HANDLE, ">>$LOGFILE_NAME") || die "could not append to logfile: $!";
	debug(1,"ldapsmb version $VERSION\n");
	debugadd(1,"   Copyright 2003-2005 Günther Deschner, Lars Müller\n");

}


sub close_logging {

	if ($USE_LOGFILE) {
		close (LOGFILE_HANDLE) || die "could not close file: $!";
	}
	if ($USE_SYSLOG) {
		closelog();
	}
}

sub set_on_find($$$) {

	my ($line, $parm, $VAR) = @_;
	debug(20,("set_on_find: line: [$line], parm: [$parm], VAR: [$VAR]\n"));
	if ($line =~ /$parm/ && $VAR eq "") {
		my ($dummy,$FOUND_VAR) = split(/.*=\s+/,$line);
		$FOUND_VAR =~ s/^\"(.*)\"$/$1/;
		debugadd(10,"autodetected \"$parm\":\t\t$FOUND_VAR\n");
		return $FOUND_VAR;
	}
	return $VAR;
}
		
sub parse_conf_file($) {

	my $handle = shift;

	# open handle and read
	open (DATA, "$handle") || die "could not open \"$handle\": $!";
	while (my $line = <DATA>) {
		next if $line =~ /^#|^;/;
		chomp($line);
		
		$ADMIN_DN 	 = set_on_find($line, "ldap admin dn", $ADMIN_DN);
		$SUFFIX		 = set_on_find($line, "ldap suffix", $SUFFIX);
		$SUFFIX_MACHINES = set_on_find($line, "ldap machine suffix", $SUFFIX_MACHINES);
		$SUFFIX_USERS	 = set_on_find($line, "ldap user suffix", $SUFFIX_USERS);
		$SUFFIX_GROUPS	 = set_on_find($line, "ldap group suffix", $SUFFIX_GROUPS);
		$SUFFIX_IDMAP	 = set_on_find($line, "ldap idmap suffix", $SUFFIX_IDMAP);
		$LDAP_SSL	 = set_on_find($line, "ldap ssl", $LDAP_SSL);
		$ADMIN_PW	 = set_on_find($line, "ldap admin password", $ADMIN_PW);
		$WORKGROUP	 = set_on_find($line, "workgroup", $WORKGROUP);

		# the following two options are only in 2.2.x
		if ($SAMBA_22) {
			$LDAP_SERVER = set_on_find($line, "ldap server", $LDAP_SERVER);
			$LDAP_PORT   = set_on_find($line, "ldap port", $LDAP_PORT);
		}
		# the following option is in HEAD/3.0 only
		# FIXME: handle more ldapsam-backends
		# FIXME: accept predefined variables (see above)
		if ($SAMBA_30 && ($line =~ /passdb backend/)) {
			
			my ($tmp, $dummy);

			($dummy,$passdb) = split(/.*=\s+/,$line);

			if ($passdb =~ /^NDS_ldapsam/) {
				$LDAP_IS_NDS = 1;
			}

			#remove leading backends
			($dummy,$tmp) = split(/ldapsam:/, $passdb);

			if (!$tmp) {
				goto set_default;
			}

			#remove trailing backends
			$tmp =~ s/(,\s+|\s+).*//g;

			my ($proto,@str) = split(/:/,$tmp);

			# remove leading slashes
			$str[0] =~ s#^//##;

			if ($proto eq "ldapi") {
				$LDAP_SERVER = "";
				$LDAP_PORT = "";
				goto found_passdb;
			} elsif ($proto eq "ldap") {
				($LDAP_SERVER,$LDAP_PORT) = @str;
				$LDAP_PORT = $LDAP_PORT  || "389";
			} elsif ($proto eq "ldaps") {
				($LDAP_SERVER,$LDAP_PORT) = @str;
				$LDAP_PORT = $LDAP_PORT  || "636";
				$LDAP_SSL = "yes";
			}
 set_default:
			$LDAP_SERVER = set_default_on_empty($LDAP_SERVER, 
							    "ldap server", 
							    "localhost", "localhost");
 found_passdb:
			debugadd(10,"autodetected \"ldap server\":\t\t\"$LDAP_SERVER\"\n");
			debugadd(10,"autodetected \"ldap port\":\t\t\"$LDAP_PORT\"\n");
		} 
	}
	close(DATA);
}

sub set_default_on_empty($$$$) {

	my ($VAR, $str, $default, $default_string) = @_;
	if (! $VAR || ($VAR eq "")) {
		debugadd(10,"no \"$str\" available. Using value of $default_string instead.\n");
		return $default;
	}
	return $VAR;
}

# parse the smb.conf
sub parse_smbconf {

	# extra conf file?
	if ($opt_smbconfig) {
		debug(3,"parsing [$opt_smbconfig]\n");
		parse_conf_file("< $opt_smbconfig");
	} elsif ($opt_smbconfig ne $LDAPSMBRC && -e $LDAPSMBRC) {
		debug(3,"parsing [$LDAPSMBRC]\n");
		parse_conf_file("< $LDAPSMBRC");
	}

	debug(3,"parsing [testparm]\n");
	parse_conf_file("$progs{testparm} -s 2> /dev/null |");

	if ($SAMBA_30 && !$passdb) {
		debug(3,"no \"passdb backend\" in your configuration. using defaults for LDAP_SERVER and LDAP_PORT\n");
	}

	# exit on missing critical info
	if (!$SUFFIX) {
		debug(0,"no \"ldap suffix\" defined. exiting.\n");
		return False;
	}

	if (!$ADMIN_DN) {
		debug(0,"no \"ldap admin dn\" defined. exiting.\n");
		return False;
	}	

	$SUFFIX_USERS = set_default_on_empty($SUFFIX_USERS, "ldap user suffix", $SUFFIX, "ldap suffix");
	$SUFFIX_GROUPS = set_default_on_empty($SUFFIX_GROUPS, "ldap group suffix", $SUFFIX, "ldap suffix");
	$SUFFIX_MACHINES = set_default_on_empty($SUFFIX_MACHINES, "ldap machine suffix", $SUFFIX, "ldap suffix");

	# have start_tls ?
	if ( $LDAP_SSL =~ /start.*tls/i ) {
		$START_TLS = 1;
	}

	# have nds ?
	if ( $LDAP_IS_NDS ) {
		$STRUCT_OC_POSIX_ACCOUNT = "inetOrgPerson";
		$STRUCT_OC_POSIX_GROUP = "groupOfNames";
	}

	# build the groups suffix
#	$SUFFIX_GROUPS = $SUFFIX_GROUPS || "ou=Groups,$SUFFIX";
#	debug(10,"will search/store/modify groups in:\t$SUFFIX_GROUPS\n");

	$SUFFIX_USERS = check_suffix($SUFFIX_USERS);
	$SUFFIX_GROUPS = check_suffix($SUFFIX_GROUPS);
	$SUFFIX_MACHINES = check_suffix($SUFFIX_MACHINES);

	return True;
}

sub check_suffix {
	
	my $suffix = shift;
	return sprintf("%s", $suffix !~ /$SUFFIX/i ? "$suffix, $SUFFIX" : "$suffix" );
}


sub find_adminpwd_in_tdb {

	# why don't we just read with tdbtool ? (Lars probably has the query key handy)

	my $key_pattern;
	my $match_pattern;
	my $subst_pattern;

	my ($dummy, $tdb_pwd);

	if ($opt_mode ne "local") {
		debug(10,"cannot retrieve password from secrets.tdb when not in local mode\n");
		goto failed;
	}

	if (! -e $confs{'secrets.tdb'} ) {
		goto failed;
	}

	if ( $SAMBA_22 ) {
		$key_pattern = "(uid|cn)=";
		$match_pattern = "\/";
		$subst_pattern = ",";
	}

	if ( $SAMBA_30 ) {
		$key_pattern = "SECRETS/LDAP_BIND_PW/";
		$match_pattern = $key_pattern;
		$subst_pattern = "";
	}
	my $cnt = 0;

	open (DATA, "$progs{tdbdump} $confs{'secrets.tdb'} 2> /dev/null|") || die "could not open $confs{'secrets.tdb'}: $!";
	while (my $line = <DATA>) {
		
		next if ($line =~ /^(}|{)/ );
		
		debug(20,"line is: $line\n");


		if ($line =~ /^key.*= \"$key_pattern/) {
			my ($dummy,$tmp) = split(/"/,$line);
			$tmp =~ s/$match_pattern/$subst_pattern/g;
			if ($tmp eq $ADMIN_DN) {
				$cnt = 1; 
				next;
			} else {
				debug(3,"strange dn: \"$tmp\"\n");
			}
		} elsif ($cnt == 1) {
			($dummy,$tdb_pwd) = split(/\"/,$line);
			$tdb_pwd =~ s/\\.*$//g;
			if ($DEBUG_PASSWORD) {
				debug(10,"autodetected \"ldap admin password\":\t$tdb_pwd\n");
			} else {
				debug(10,"autodetected \"ldap admin password\":\tnot logged...\n");
			}
			return $tdb_pwd;
		} 
	}
	close(DATA);
	
	if ( $cnt == 0 ) {
		debug(0,"sorry. could not find your password in \"secrets.tdb\"\n");
		debugadd(0,"either you set it in [$0] or you use \"smbpasswd -w pwd\" if you are running [$0] in local mode.\n");
		debugadd(0,"exiting.\n");
	}

 failed:
	return undef;
}

#@ gets a list of binaries
#@ returns != 0 on failure
sub find_progs (@) {

	my @progs = @_;

	my $path = sprintf("%s:%s", $ENV{PATH}, "/usr/sbin");
	my @paths = split(/:/, $path);
	foreach my $bin (@progs) {
		foreach my $path (@paths) {
			if ( -e "$path/$bin" ) {
				$progs{$bin} = "$path/$bin";
				last;
			}
		}
		if (! $progs{$bin} ) {
			debug(0,"could not find $bin\n");
			return False;
		}
	}
	return True;
}

sub find_confs (@) {

	my @confs = @_;

	my @paths = qw( /etc /etc/samba );
	foreach my $conf (@confs) {
		foreach my $path (@paths) {
			if ( -e "$path/$conf" ) {
				$confs{$conf} = "$path/$conf";
				last;
			}
		}
		if (! $confs{$conf}) {
			debug(0,"could not find $conf\n");
			return False;
		}
	}
	return True;
}


sub get_smbd_samba_ver ($) {

	my $smbd = shift || return undef;
	
	my $samba_ver = `$smbd -V`;	
	$samba_ver =~ s/^version //i;

	if ( $samba_ver =~ /^2/ ) {
		return "2_2";
	} elsif ( $samba_ver =~ /^3|3\.0/ ) {
		return "3_0";
	} elsif ( $samba_ver =~ /HEAD/ ) {
		return "HEAD";
	} elsif ( $samba_ver =~ /tng/i ) {
		debug(0,"Samba TNG is not yet supported. exiting.\n");
		return undef;
	} 
	
	debug(0,"unknown samba version. exiting.\n");
	return undef;
}


sub check_sane_name {

	my $tmp = shift;

	if ( $tmp =~ /\*/ ) { 
		return "an asterik is a wildcard character" 
	};
	if ( $tmp =~ /\./ ) { 
		return "dots can possibly cause problems if you want to do mail-routing later" 
	};
	if (! $HAVE_UTF8) {
		if ( $tmp =~ /ä|ö|ü|ß/i ) { 
			return "german umlauts are depreciated unless your system is fully utf8-aware" 
		};
	}
	if ( $tmp =~ /\$/ ) { 
		return "the \$-sign should stay reserved for workstations" 
	};
	if ( $tmp =~ /\s+/ ) { 
		return "white-space in names can cause some applications to behave oddly" 
	};
}


sub check_rid_allocation {

	my $uidNumber 	= shift;
	my $gidNumber 	= shift;
	my $rid 	= shift;
	my $primaryGroupID = shift;

	if ( $rid != $uidNumber*2+1000 ) {
		return "warning: your user-RID does not match.\nthis account may be broken!"
	}

	if ( $primaryGroupID != $gidNumber*2+1001 ) {
		return "warning: your group-RID does not match.\nthis account may be broken!"
	}
}

sub get_schema_oc_exist($$$) {

	my $hd = shift || return undef;
	my $schema = shift || $hd->schema() || return undef;
	my $oc = shift;

	return $schema->objectclass($oc);
}

sub get_schema_attr_exist($$$) {

	my $hd = shift || return undef;
	my $schema = shift || $hd->schema() || return undef;
	my $attr = shift;

	return $schema->attribute($attr);
}

sub get_schema_samba_ver($$) {

	my $hd = shift || return undef;
	my $schema = shift || $hd->schema() || return undef;
	
	if (get_schema_attr_exist($hd, $schema, 'sambaSID')) {
		return "3_0"; 
	} elsif (get_schema_attr_exist($hd, $schema, 'rid')) {
		return "2_2";
	} else {
		debug(0,"unknown samba-version\n");
		return undef;
	}
}

sub get_schema_check_ocs($$@) {

	my $hd = shift || return undef;
	my $schema = shift || $hd->schema() || return undef;

	my @ocs = @_;

	for (my $i=0; $ocs[$i]; $i++) {
		if (get_schema_oc_exist($hd, $schema, $ocs[$i])) {
			return $ocs[$i];
		}
	}
	return undef;

}

sub get_schema_struct_group_oc($$) {

	my $hd = shift;
	my $schema = shift;
	my @ocs = ( "groupOfNames", "namedObject", undef);
	return get_schema_check_ocs($hd, $schema, @ocs);
}

sub get_schema_struct_user_oc($$) {

	my $hd = shift;
	my $schema = shift;
	my @ocs = ( "account", "inetOrgPerson", undef);
	return get_schema_check_ocs($hd, $schema, @ocs);
}

sub detect_mode($) {

	my $mode = shift;

	$mode = $opt_mode || "local";
	if ( $mode =~ /(local|remote)/ ) {
		$opt_mode = $mode;
	} else {
		debug(0,"unknown mode choosen: $mode\n");
		return False;
	}
	return True;
}


sub prepare_account($) {

	my $hd = shift;
	
	if ($opt_gid) {
		$opt_gid = get_gid($hd, $opt_gid);
		if (!$opt_gid) {
			return False;
		}
	}

	# handle umlauts (unless utf8 is widly adopted...)
	if ($opt_user) {
		if (! $opt_force && $opt_add ) {
			my $ret = check_sane_name($opt_user);
			if ($ret) { 
				debug(0,"your username ($opt_user) has strange characters that might cause problems:\n");
				debug(0,"\t$ret\n");
				debug(0,"you can force account creation with -f\n");
				return False;
			}
		}

		if (! $HAVE_UTF8) {
			my $tmp = $opt_user;
			from_to($opt_user, "utf-8", "iso-8859-1" );
			$opt_user = encode_utf8($opt_user);
			$ascii_user = umlaut2ascii($tmp);
		} else {
			# TODO: quick fix - this needs more testing and better naming:
			$ascii_user = $opt_user;
		}
	}

	if ($opt_group) {
		if (! $opt_force && $opt_add) {
			my $ret = check_sane_name($opt_group);
			if ($ret) { 
				debug(0,"your groupname ($opt_group) has strange characters that might cause problems:\n");
				debug(0,"\t$ret\n");
				debug(0,"you can force account creation with -f\n");
				return False;
			}
		}
		if (! $HAVE_UTF8) {
			my $tmp = $opt_group;
			from_to($opt_group, "utf-8", "iso-8859-1" );
			$opt_group 	= encode_utf8($opt_group);
			$ascii_group = umlaut2ascii($tmp);
		} else {
			# TODO: quick fix - this needs more testing and better naming:
			$ascii_group = $opt_group;
		}
	}
	return True;
}

sub get_domsid ($$) {

	my $hd = shift;
	my $workgroup = shift;
	my $attr = 'sambaSid';

	my $num_result = ldap_search($hd, $SUFFIX, "(&(objectclass=sambaDomain)(sambaDomainName=$workgroup))", $attr, "one");
	if ($num_result != 1) {
		debug(1,"found $num_result sambaDomain objects below $SUFFIX\n");
		return undef;
	}

	return $entry->get_value('sambaSid') || undef;
}

sub get_gid ($) {

	# handle gid
	my $hd = shift;
	my $gid = shift;
	my $gid_ret;

	if ( defined($gid) && ( $gid =~ /^\d+$/ ) ) {
		return $gid;
	}
	
	debug(5,"gid $gid is not numeric. converting\n");
	my $num_result = ldap_search($hd, $SUFFIX,"(&(objectclass=posixGroup)(cn=$gid))");
	if ($num_result > 1) {
		debug(0,"more then one groups with name [$gid] found\n");
		return undef;
	}
	if ($num_result == 1) {
		return $entry->get_value('gidNumber') || undef;
	}
	
	$gid_ret = getgrnam($gid);
	if ($gid_ret || defined($gid_ret)) {
		debug(5,"ok. group [$gid] exists somewhere else in the NSS.\n");
		return $gid_ret;
	} 
	debug(0,"could not resolve group: [$gid] to numeric gid\n");
	return undef;
}

sub refresh_nscd {
	my $ret;

	if ($> != 0) {
		debug(10,"not root, cannot restart nscd\n");
		return False;
	}

	if (! -e $NSCD_SOCKET || ! -S $NSCD_SOCKET) {
		debug(10,"no nscd running\n");
		return True;
	}
	if (! -e $NSCD_INIT_SCRIPT) {
		debug(1,"no init script to reload nscd\n");
		return False;
	}

	debug(1,"reloading nscd\n");

	$ret = system("$NSCD_INIT_SCRIPT restart >/dev/null 2>&1");
	if ($ret) {
		debug(1,"failed to reload nscd\n");
		return False;
	}

	return True;
}


__END__

=head1 NAME

B<ldapsmb> - LDAP-Managment-Tool for a Samba Domain Controller

=head1 SYNOPSIS

ldapsmb [options] 

	Main Options:
--add|-a				Add something
--config				Show configuration
--delete|-d				Delete something
--group|-g <groupname>			Set Group-Name
--help|-h|?				Display help
--init|-i				Initialize LDAP 
--join|-j				Join a user to a group
--list|-l				List Something
--modify|-m				Modify something
--remove|-r				Remove a user from a group
--smbacct|-s				Promote to samba-Account
--user|-u <username>			Set User-Name
--workstation|-wks <workstationname>	Set Workstation-Name

	Global Options:
--bindpw <password>			LDAP bind password
--debug <n>				Select debug-level (default: 3)
--force|-f				Force execution
--mode					Select mode (default: local)
--quiet|-q				No output
--raw					Raw list-output
--smbconf <smb.conf>			Choose another configfile
--verbose|-v				Verbose output
--version				Display version
--logfile <logfile>			Specify logfile (default: @LOGFILE@)

	Misc Options:
--comment|-c <comment>			Set Comment
--gid <n>				Set Gid-Number
--homedir <home directory>		Set Home-Directory
--makehomedir				Make Home-Directory
--ntgroup <ntgroupname>			Set NT-Groupname
--passwd <password>			Set Password 
--rid <n>				Set Rid for Samba account
--shell <login shell>			Set Loginshell
--skeldir|-k <skeleton dir>		Define Skeleton-Dir
--uid <n>				Set Uid-Number
--username <username>			Set Username

=head1 DESCRIPTION

B<ldapsmb> will create and delete Posix-Accounts for users, groups and
workstations in your LDAP-Directory. Although designed for Samba 3.0/HEAD it
should work for Samba 2.2.x as well. Furthermore B<ldapsmb> should provide all
necessary scripting-hooks to fullfill a clean "net rpc vampire" - Migration of a
NT4/2000 Domain Controller to a Samba 3.0 PDC. 

B<ldapsmb> can run in two modes:

	local:	your smbd is running on the same machine where B<ldapsmb> is called.
	remote:	your smbd is running on another machine.

All LDAP relevant configuration data will be autodetected if possible. Your
password can be autodetected as well, as long as you have read permission on
your secrets.tdb where your admin password will be stored after you have called
B<smbpasswd -w adminpassword>. If you have not done that (e. g. running in
remote mode) you have to set the password manually in B<ldapsmb>.

The file C<~/.ldapsmbrc> could be used to set additional configuration
parameters not yet part of the smb.conf.

=head1 MAIN OPTIONS

=over 8

=item B<--add|-a>

Add an account. Requires B<--user|-u>, B<--group|-g> or B<--workstation|-wks>. Can be
combined with B<-smbacct> do promote the posixAccount to a full sambaAccount if
running in local-mode.

=item B<--config>

Show the config that will be used (the result of all autodetections).

=item B<--delete|-d>

Delete an account. Requires B<--user|-u>, B<--group|-g> or
B<--workstation|-wks>.

=item B<--group|-g> C<groupname>

Define a groupname. Requires B<-add>, B<-delete>, B<-list>, B<-join> or
B<-remove>.

=item B<--help|-h>

Print a brief help message and exits.

=item B<--init|-i>

to be documented...

=item B<--join|-j>

Join a LDAP-PosixAccount to a LDAP-PosixGroup. Requires B<--user|-u> and
B<--group|-g>. A corresponding memberUid-attribute with the given username will
be added to the posixGroup-object.

=item B<--list|-l>

List an account. Requires B<--user|-u>, B<--group|-g> or B<--workstation|-wks>.
If no user, group or workstation is specified, all entries of the specific
account are listed.

=item B<--mode> C<mode>

Choose in which mode B<ldapsmb> should run (local, remote). If running in local
mode (the default) it will check for ldap-support in smbd, look for a
secrets-tdb to retrieve the password, etc. If running in remote-mode, you need
to assign a config-file with --smbconf /path/to_my/smb.conf and you need to
write your admin-password into B<ldapsmb>. Adding or promoting Accounts to full
LDAP-Accounts is currently only available in local mode.

=item B<--modify|-m>

Modify an account. Requires B<--user|-u>, B<--group|-g> or
B<--workstation|-wks>.

=item B<--raw>

Raw output while listing.

=item B<--smbacct>

Trigger all necessary steps to make either a user, a group or a workstation a
full Samba-Account.  This option will be redesigned in the future. It was added
by Lars Müller to ease Samba 2.2-Administration but really makes not very much
sense for Samba 3.0. By default samba-specific information are not added.

=item B<--remove|-r>

Remove a LDAP-PosixAccount from a LDAP-PosixGroup. Requires B<-user> and
B<-group>. If existant, the memberUid-attribute for a given username will be
removed from the posixGroup-object.

=item B<--user|-u> C<username>

Define a username. Requires B<-add>, B<-delete>, B<-list>, B<-join> or
B<-remove>.

=item B<--workstation|-wks> C<workstation>

Define a workstation. Requires B<-add>, B<-delete>, B<-list>, B<-join> or
B<-remove>.

=head1 GLOBAL OPTIONS

=item B<--debug n>

Use an debuglevel. You can choose between 3 (default), 5 and 10 (full
debugging).

=item B<--force|-f> 

Force execution.

=item B<--quiet|-q>

No output.

=item B<--smbconf> F<smb.conf>

Use another smb.conf-file instead of the default location. Needed for running
in remote mode.

=item B<--verbose|-v>

Prints additional information if possible.

=item B<--version>

Prints the version and exits.

=head1 MISC OPTIONS

=item B<--comment|-c> C<comment>

Set a comment for a given user while adding or modifying a user.

=item B<--homedir> C<homedir>

Set the user's login directory while adding or modifying a user. The default
is to append the login name to default_home and use that as the login directory
name.

=item B<--makehomedir>

Create user's home directory while adding a user. The files contained in
/etc/skeleton will be copied to the home directory.

=item B<--passwd> C<pwd>

Set a password for a given user while adding or modifying a user. If no
password is set, the password is identical with the userid (FIXME).

=item B<--shell> C<shell>

Set the login shell while adding or modifying a user. The default is to leave
this field blank, which causes the system to select the default login shell.

=item B<--skeldir skeldir>

Set a different skeleton directory while using the B<-makehomedir> option.

=back

=head1 EXAMPLES

C<ldapsmb -a -u gd -passwd 'secret' -smbacct> 

This will create a user called gd in LDAP and will add all required information
to make that a full samba-Account.

C<ldapsmb -j -u lmuelle -g admins> 

This will add the user lmuelle to the admins-group in ldap.

=head1 AUTHOR

Günther Deschner <gd@suse.de>
Lars Müller <lmuelle@suse.de>
Björn Jacke <bjoern@j3e.de>

If you find any errors in the code please let me know at gd@suse.de.

=head1 BUGS

B<ldapsmb> is not yet fully UNICODE-aware. B<ldapsmb> does not yet deal with
idmap in ldap. Both features will be added in a future release.

=head1 VERSION

This is version 1.34b of B<ldapsmb>.

=head1 COPYRIGHT

Copyright (c) 2003-2005 SuSE Linux AG. All rights reserved.

This program is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.

=cut
