/**
 * File:	modules/Ldap.ycp
 * Module:	Configuration of LDAP client
 * Summary:	LDAP client configuration data, I/O functions.
 * Authors:	Thorsten Kukuk <kukuk@suse.de>
 *		Anas Nashif <nashif@suse.de>
 *
 * $Id: Ldap.ycp 22232 2005-03-03 15:31:01Z jsuchome $
 */

{

    module "Ldap";
    textdomain "ldap-client";

    import "Arch";
    import "Autologin";
    import "Label";
    import "Message";
    import "Mode";
    import "Nsswitch";
    import "Package";
    import "PamSettings";
    import "Popup";
    import "Progress";
    import "Report";
    import "Service";
    import "Stage";
    import "Summary";

    /**
     * show popups with error messages?
     */
    global boolean use_gui	= true;

    /**
     * DN of base configuration object
     */
    global string base_config_dn = "";


    include "ldap/routines.ycp";

    /**
     * Required packages for this module to operate
     * -- they are now required only when LDAP is set for authentication
     */
    global list<string> required_packages = [];

    /**
     * Write only, used during autoinstallation.
     * Don't run services and SuSEconfig, it's all done at one place.
     */
    global boolean write_only = false;

    /**
     * Are LDAP services available via nsswitch.conf?
     */
    global boolean start	= false;
    global boolean old_start	= false;

    /**
     * Is NIS service available? If yes, and LDAP client will be enabled, warn
     * user (see bug #36981)
     */
    global boolean nis_available	= false;

    /**
     * If no, automounter will not be affected.
     */
    global boolean _autofs_allowed = true;
    /**
     * Start automounter and import the settings from LDAP
     */
    global boolean _start_autofs = false;

    /**
     * If login of LDAP uses to local machine is enabled
     */
    global boolean login_enabled = true;

    /**
     * which attribute have LDAP groups for list of members
     */
    global string member_attribute	= "";
    global string old_member_attribute	= "";

    /**
     * IP addresses of LDAP server.
     */
    global string  server = "";
    global string  old_server = "";

    // local settings modified?
    global boolean modified = false;

    // /etc/openldap/ldap.conf modified?
    global boolean openldap_modified = false;

    // base DN
    string domain = "";
    string old_domain = nil;
    boolean domain_changed = false;

    /* Do we have an v2 or v3 ldap server? */
    global boolean ldap_v2 = false;
    global boolean ldap_tls = false;

    /* Which crypt method should be used? If not set, use crypt.
       Not configurable now! TODO */
    global string pam_password = "";

    // lines of /etc/passwd, starting with +/-
    global list<string> plus_lines_passwd	= [];

    global integer default_port = 389;

    /**
     * If home directories of LDAP users are stored on this machine
     */
    global boolean file_server = false;

    // settings from ldap.conf
    global string nss_base_passwd = "";
    global string nss_base_shadow = "";
    global string nss_base_group = "";
    // settings from LDAP configuration objects
    global string user_base = "";
    global string group_base = "";

    /* stored values of /etc/nsswitch.conf */
    map nsswitch = $[
	"passwd" : [], "group": [], "passwd_compat": [], "group_compat": []
    ];

    // are we binding anonymously?
    global boolean anonymous = false;

    // bind password for LDAP server
    global string bind_pass = nil;

    /**
     * DN for binding to LDAP server
     */
    global string bind_dn = "";

    // DN of currently edited configuration module
    global string current_module_dn = "";
    // DN of currently edited template
    global string current_template_dn = "";

    // if LDAP configuration objects should be created automaticaly
    global boolean create_ldap	= false;

    // if authentication is currently done via Kerberos
    global boolean krb_first		= false;

    // if kerberos authentication should remain primary one
    global boolean krb_authentication	= nil;

    // current pam modules called by pam_unix (value of call_modules=... in
    // /etc/security/pam_unix2)
    list<string> auth_modules = [];

    /**
     * defaults for adding new config objects and templates
     */
    global map new_objects = $[
	"suseuserconfiguration"	: $[
	    "susesearchfilter"	: ["objectclass=posixaccount"],
	    "susepasswordhash"	: ["CRYPT"],
	    "suseskeldir"	: ["/etc/skel"],
	    "suseminuniqueid"	: ["1000"],
	    "susenextuniqueid"	: ["1000"],
	    "susemaxuniqueid"	: ["60000"],
	    "suseminpasswordlength" : ["5"],
	    "susemaxpasswordlength" : ["8"],
	],
	"susegroupconfiguration": $[
	    "susesearchfilter"	: ["objectclass=posixgroup"],
	    "suseminuniqueid"	: ["1000"],
	    "susenextuniqueid"	: ["1000"],
	    "susemaxuniqueid"	: ["60000"],
	],
	"suseusertemplate"	: $[
	    "objectclass"	: [ "top", "suseobjecttemplate", "suseusertemplate"],
	    "susenamingattribute"	: [ "uid" ],
	    "susedefaultvalue"	: [
		"homedirectory=/home/%uid",
		"loginshell=/bin/bash"
	    ],
	    "suseplugin"		: [ "UsersPluginLDAPAll" ]
	],
	"susegrouptemplate"	: $[
	    "objectclass"	: [ "top", "suseobjecttemplate", "susegrouptemplate"],
	    "susenamingattribute"	: [ "cn" ],
	    "suseplugin"		: [ "UsersPluginLDAPAll" ]
	]
    ];

    global string base_template_dn = base_config_dn;

    // settings saved at LDAP server modified
    global boolean ldap_modified = false;

    global map config_modules = $[];
    global map templates = $[];

    global boolean bound = false;

    // DN's of groups (posixGroups) in LDAP
    global list groups_dn = [];

    /**
     * Map of object classes (from schema). Indexed by names.
     */
    global map object_classes = $[];

    /**
     * Map of atribute types (from schema). Indexed by names.
     */
    global map attr_types = $[
    ];

    /**
     * encryption schemes supported by slappasswd
     */
    global list hash_schemas = ["CLEAR", "CRYPT", "SHA", "SSHA", "MD5", "SMD5"];

    /**
     * Available configuration modules (objectclass names)
     * TODO update
     */
    global list<string> available_config_modules = [
	"suseuserconfiguration",
	"susegroupconfiguration"
    ];

    /**
     * The defualt values, which should replace the ones from Read ()
     * Used during instalation, when we want to do a reasonable proposal
     */
    global map initial_defaults = $[];

    /**
     * If the default values, used from ldap-server module were used
     * to configure ldap-client
     */
    global boolean initial_defaults_used = false;

    global boolean schema_initialized	= false;

    global boolean ldap_initialized	= false;

    /**
     * if sshd should be restarted during write phase
     */
    global boolean restart_sshd		= false;

    // if /etc/passwd was read
    boolean passwd_read			= false;

    // packages to install for specific arhitectures (#49808)
    global map arch_packages	= $[
	"pam_ldap"	: $[
	    "ia64"	: [ "pam_ldap-x86" ],
	    "ppc"	: [ "pam_ldap-64bit" ],
	    "ppc64"	: [ "pam_ldap-32bit" ],
	    "s390_64"	: [ "pam_ldap-32bit" ],
	    "x86_64"	: [ "pam_ldap-32bit" ],
	],
	"nss_ldap"	: $[
	    "ia64"	: [ "nss_ldap-x86" ],
	    "ppc"	: [ "nss_ldap-64bit" ],
	    "ppc64"	: [ "nss_ldap-32bit" ],
	    "s390_64"	: [ "nss_ldap-32bit" ],
	    "x86_64"	: [ "nss_ldap-32bit" ],

	]
    ];


    //----------------------------------------------------------------

    /**
     * If the domain has changed from a nonempty one, it may only be
     * changed at boot time. Use this to warn the user.
     * @return whether changed by SetDomain
     */
    global define boolean DomainChanged () ``{
	return domain_changed;
    }

    /**
     * @return Get the LDAP domain.
     */
    global define string GetDomain () ``{
	return domain;
    }

    /**
     * Set the LDAP domain.
     * @param new_domain a new domain
     */
    global define void SetDomain (string new_domain) ``{
	domain = new_domain;
	if (domain != old_domain && old_domain != "")
	{
	    domain_changed = true;
	}
    }

    /**
     * Set the defualt values, which should replace the ones from Read ()
     * Used during instalation, when we want to do a reasonable proposal
     */
    global define boolean SetDefaults (map settings) ``{

	initial_defaults = eval (settings);
	return true;
    }

    /* ------------- auto_yast functions -------------------------------- */

    /**
     * Only set variables, without checking anything
     * @return: void
     */
    global define void Set (map settings)  ``{
	start		= settings ["start_ldap"]:false;
	server		= settings ["ldap_server"]:"";
	domain		= settings ["ldap_domain"]:"";
	ldap_v2		= settings ["ldap_v2"]:false;
	ldap_tls	= settings ["ldap_tls"]:false;
	pam_password	= settings ["pam_password"]:"crypt";
	bind_dn		= settings ["bind_dn"]:"";
	file_server	= settings ["file_server"]:false;
	base_config_dn	= settings ["base_config_dn"]:"";
        nss_base_passwd = settings ["nss_base_passwd"]:"";
        nss_base_shadow = settings ["nss_base_passwd"]:"";
        nss_base_group	= settings ["nss_base_group"]:"";
	member_attribute= settings ["member_attribute"]:"member";
	create_ldap	= settings ["create_ldap"]:false;

	_autofs_allowed = haskey (settings, "start_autofs");
	_start_autofs	= settings["start_autofs"]:false;
	if (_start_autofs)
	    required_packages = (list<string>)
		union (required_packages, ["autofs4"]);

	old_domain		= domain;
	old_server		= server;
	old_member_attribute	= member_attribute;
	modified		= true;
	openldap_modified	= true;
	return;
    }

    /**
     * Get all the LDAP configuration from a map.
     * When called by ldap_auto (preparing autoinstallation data)
     * the map may be empty.
     * @param settings	$["start": "domain": "servers":[...] ]
     * @return	success
     */
    global define boolean Import (map settings) ``{
	Set (settings);
	return true;
    }

    /**
     * Dump the LDAP settings to a map, for autoinstallation use.
     * @return $["start":, "servers":[...], "domain":]
     */
    global define map Export () ``{
	map e = $[
	    "start_ldap"	: start,
	    "ldap_server"	: server,
	    "ldap_domain"	: domain,
	    "ldap_v2"		: ldap_v2,
	    "ldap_tls"		: ldap_tls,
	    "bind_dn"		: bind_dn,
	    "file_server"	: file_server,
	    "base_config_dn"	: base_config_dn,
	    "nss_base_passwd"	: nss_base_passwd,
	    "nss_base_shadow"	: nss_base_shadow,
	    "nss_base_group"	: nss_base_group,
	    "pam_password"	: pam_password,
	    "member_attribute"	: member_attribute,
	    "create_ldap"	: create_ldap
	];
	if (_autofs_allowed)
	    e["start_autofs"]	= _start_autofs;
	return e;
    }

    /**
     * Summary()
     * returns html formated configuration summary
     * @return summary
     */
    global define string Summary ()
	``{
	string summary = "";
	summary = Summary::AddHeader(summary, _("LDAP Client enabled"));
	summary = Summary::AddLine(summary, (start) ? "Yes" : Summary::NotConfigured());
	summary = Summary::AddHeader(summary, _("LDAP Domain"));
	summary = Summary::AddLine(summary, (domain != "") ? domain : Summary::NotConfigured());
	summary = Summary::AddHeader(summary, _("LDAP Server"));
	summary = Summary::AddLine(summary,( server!="") ? server : Summary::NotConfigured());
	summary = Summary::AddHeader(summary, _("LDAP Version 2"));
	summary = Summary::AddLine(summary, (ldap_v2) ? "Yes" : Summary::NotConfigured());
	summary = Summary::AddHeader(summary, _("LDAP TLS/SSL"));
	summary = Summary::AddLine(summary, (ldap_tls) ? "Yes" : Summary::NotConfigured());

	return summary;
    }

    /**
     * returns html formated configuration summary (shorter than Summary)
     * @return summary
     */
    global define string ShortSummary () ``{
	string nc = Summary::NotConfigured();
	string summary = "";
	// summary text
	summary = sformat (_("<b>Servers</b>:%1<br>"), server!=""? server: nc) +
	// summary text
	sformat (_("<b>Base DN</b>:%1<br>"), domain != "" ? domain : nc) +
	// summary text (yes/no follows)
	sformat (_("<b>Client Enabled</b>:%1<br>"), start ? _("Yes") : _("No"));
	return summary;
    }

    /* ------------- read/write functions ------------------------------- */

    /**
     * Read single entry from /etc/ldap.conf file
     * @param entry entry name
     * @param defvalue default value if entry is not present
     * @return entry value
     */
    define string ReadLdapConfEntry (string entry, string defvalue) ``{

	string value = defvalue;
	any ret = (any) SCR::Read(
	    add (.etc.ldap_conf.v."/etc/ldap.conf", entry));
	if (ret == nil)
	    value = defvalue;
	else if (is (ret,list))
	{
	    value = ((list)ret)[0]:defvalue;
	}
	else
	    value = sformat ("%1", ret);
	return value;
    }

    /**
     * Read multi-valued entry from /etc/ldap.conf file
     * @param entry entry name
     * @return entry value
     */
    define list<string> ReadLdapConfEntries (string entry) ``{

	any ret = (any) SCR::Read(
	    add (.etc.ldap_conf.v."/etc/ldap.conf", entry));
	if (ret == nil)
	    return [];
	else if (is (ret,list))
	{
	    return (list<string>) ret;
	}
	else
	    return [ sformat ("%1", ret) ];
    }

    /**
     * Write (single valued) entry to /etc/ldap.conf
     * @param entry name
     * @param value
     */
    define void WriteLdapConfEntry (string entry, string value) ``{

	SCR::Write (add (.etc.ldap_conf.v."/etc/ldap.conf",entry), [value]);
    }

    /**
     * Write (possibly multi valued) entry to /etc/ldap.conf
     * @param entry name
     * @param value it is of type [attr1, attr2],
     * in /etc/ldap.conf should be written as "entry attr1 attr2"
     * @example to write "nss_map_attribute       uniquemember member", call
     * WriteLdapConfEntries ("nss_map_attribute", ["uniquemember", "member"])
     */
    define void WriteLdapConfEntries (string entry, list<string> value) ``{

	list<string> current	= ReadLdapConfEntries (entry);
	list<string> values	= [];
	foreach (string val, current, ``{
	    list lval	= splitstring (val, " \t");
	    if (tolower (lval[0]:"") != tolower (value[0]:""))
		values	= add (values, val);
	    else
		values	= add (values, mergestring (value, " "));
	});
	if (size (current) == 0)
	    values	= [ mergestring (value, " ") ];
	SCR::Write (add (.etc.ldap_conf.v."/etc/ldap.conf", entry), values);
    }

    /**
     * Add a new value to the entry in /etc/ldap.conf
     * @param entry name
     * @param value
     */
    define void AddLdapConfEntry (string entry, string value) ``{

	list<string> current	= ReadLdapConfEntries (entry);
	current = maplist (string e, current, ``(tolower (e)));

	if (!contains (current, tolower (value)))
	{
	    SCR::Write (add (.etc.ldap_conf.v."/etc/ldap.conf", entry),
		union (current, [value]));
	}
    }

    /**
     * Reads LDAP settings from the SCR
     * @return success
     */
    global define boolean Read () {

	foreach (string db,["passwd","group","passwd_compat","group_compat"],``{
	    nsswitch[db]	= Nsswitch::ReadDb (db);
	});

	start = contains (nsswitch["passwd"]:[], "ldap") ||
		(contains (nsswitch["passwd"]:[], "compat") &&
		 contains (nsswitch["passwd_compat"]:[], "ldap"));

	old_start	= start;

	nis_available	= contains (nsswitch["passwd"]:[], "nis") ||
	    (contains (nsswitch["passwd"]:[], "compat") &&
	    (contains (nsswitch["passwd_compat"]:[], "nis") ||
	     size (nsswitch["passwd_compat"]:[]) == 0));
	nis_available	= nis_available && (Service::Status ("ypbind") == 0);

	server = ReadLdapConfEntry ("host", "");
	domain  = ReadLdapConfEntry ("base", "");
	old_domain = domain;
	old_server = server;

	ldap_v2 = (ReadLdapConfEntry ("ldap_version", "3") == "2");
	ldap_tls = (ReadLdapConfEntry ("ssl", "no") == "start_tls");

        nss_base_passwd = ReadLdapConfEntry ("nss_base_passwd", "");
        nss_base_shadow = ReadLdapConfEntry ("nss_base_shadow",nss_base_passwd);
        nss_base_group = ReadLdapConfEntry ("nss_base_group", "");

	pam_password = ReadLdapConfEntry ("pam_password", "crypt");

	// read sysconfig values
	base_config_dn = (string)SCR::Read (.sysconfig.ldap.BASE_CONFIG_DN);
	if (base_config_dn == nil)
	    base_config_dn = "";

	file_server = ((string)SCR::Read (.sysconfig.ldap.FILE_SERVER)== "yes");

	bind_dn = (string)SCR::Read (.sysconfig.ldap.BIND_DN);
	if (bind_dn == nil || bind_dn == "")
	    bind_dn = ReadLdapConfEntry ("binddn", "");

	list<string> map_attrs = ReadLdapConfEntries ("nss_map_attribute");
	foreach (string map_attr, map_attrs, ``{
	    map_attr		= tolower (map_attr);
	    if (issubstring (map_attr, "uniquemember"))
	    {
		list<string> attr	= splitstring (map_attr, " \t");
		if (attr[0]:"" == "uniquemember")
		{
		    member_attribute		= attr[1]:member_attribute;
		    old_member_attribute	= member_attribute;
		}
	    }
	});

	// install on demand
	_autofs_allowed = true;
	_start_autofs = _autofs_allowed && Service::Enabled ("autofs");

	// read /etc/passwd to check + line:
	if (!(boolean)SCR::Execute (.passwd.init,$["base_directory":"/etc"]))
	{
	    string error	= (string) SCR::Read (.passwd.error);
	    y2error ("error: %1", error);
	}
	else
	{
	    passwd_read		= true;
	    plus_lines_passwd	=
		(list<string>) SCR::Read (.passwd.passwd.pluslines);
	    foreach (string plus_line, plus_lines_passwd, ``{
		list<string> plus	= splitstring (plus_line, ":");
		if (plus[size (plus) -1]:"" == "/sbin/nologin")
		    login_enabled	= false;
	    });
	}

	list<string> auth = PamSettings::GetValues ("pam_unix2", "auth");
	integer	ldap_pos = -1;
	integer	krb_pos = -1;
	integer pos	= -1;
	boolean comment	= false;
	foreach (string val, auth, {
	    pos	= pos + 1;
	    if (substring (val, 0, 1) == "#")
		comment = true;
	    if (comment)
		return;
	    if (issubstring (val, "krb5"))
		krb_pos	= pos;
	    if (val == "use_ldap")
		ldap_pos = pos;
	    if (issubstring (val, "call_modules="))
	    {
		list line_split	= splitstring (val, "=");
		auth_modules	= splitstring (line_split[1]:"",",");
	    }
	});
	if (krb_pos != -1 && (ldap_pos == -1 || krb_pos < ldap_pos))
	{
	    krb_first	= true;
	}

	Autologin::Read ();

	// Now check if previous configuration of LDAP server didn't proposed
	// some better values:
	if (Stage::cont ())
	{
	    if (size (initial_defaults) > 0)
	    {
		string old_s	= old_server;
		string old_d	= old_domain;
		string old_m	= old_member_attribute;
		Set (initial_defaults);
		old_server	= old_s;
		old_domain	= old_d;
		old_member_attribute	= old_m;
	    }
	}

	if (member_attribute == "")
	{
	    member_attribute	= "member";
	    modified		= true;
	}

	return true;
    }

    /* ------------- functions for work with LDAP tree contents ------------ */

    /**
     * Error popup for errors detected during LDAP operation
     * @param type error type: binding/reading/writing
     * @param detailed error message (from agent-ldap)
     */
    global define void LDAPErrorMessage (string type, string error) ``{

        map ldap_error = $[
	// error message:
	"initialize": _("
Server could be down or unreachable."),

	// error message:
	"missing_dn": _("
Value of DN is missing or invalid."),

	// error message:
	"at_not_found": _("\nAttribute type not found.
"),

	// error message:
	"oc_not_found": _("\nObject class not found.
"),
	];

        map error_type = $[
	    // error message, more specific description follows
	    "init": _("Connection to LDAP server cannot be established."),
	    // error message, more specific description follows
	    "bind": _("Problem with connecting to the LDAP server."),
	    // error message, more specific description follows
	    "read": _("Problem with reading data from the LDAP server."),
	    // error message, more specific description follows
	    "users": _("There was a problem with writing LDAP users."),
	    // error message, more specific description follows
	    "groups": _("There was a problem with writing LDAP groups."),
	    // error message, more specific description follows
	    "write": _("There was a problem with writing data to LDAP server."),
	    // error message, more specific description follows
	    "schema": _("Problem with reading schema from the LDAP server."),
	];

	if (!use_gui)
	{
	    y2error (error_type[type]:"Unknown LDAP error");
	    y2error (ldap_error[error]:error);
	    return;
	}

	if (error == nil) error = "YaST error?";

	UI::OpenDialog (`HBox(`HSpacing (0.5),
	    `VBox(
		`VSpacing (0.5),
		// label
		`Left(`Heading (Label::ErrorMsg())),
		// default error message
		`Label (error_type[type]:_("Unknown LDAP error")),
		`ReplacePoint (`id(`rp), `Empty()),
		`VSpacing (0.5),
		`Left(`CheckBox (`id(`details), `opt (`notify),
		    // checkbox label
		    _("&Show Details"), false)),
		`PushButton (`id(`ok), `opt(`key_F10,`default),
		    Label::OKButton())
	    ),
	    `HSpacing(0.5))
	);
	any ret = nil;
	if (error == "")
	    UI::ChangeWidget (`id(`details), `Enabled, false);
	do
	{
	    ret = UI::UserInput();
	    if (ret == `details)
	    {
		if ((boolean)UI::QueryWidget (`id(`details), `Value))
		    UI::ReplaceWidget (`id(`rp), `VBox (
			`Label (ldap_error[error]:error)));
		else
		    UI::ReplaceWidget (`id(`rp), `Empty());
	    }
	}
	while (ret != `ok && ret != `cancel);
	UI::CloseDialog();
    }

    /**
     * Reads and returns error map (=message + code) from agent
     */
    global define map LDAPErrorMap () ``{

        return ((map)SCR::Read(.ldap.error));
    }

    /**
     * Reads and returns error message from agent
     */
    global define string LDAPError () ``{

	map err_map = LDAPErrorMap ();
	string error	= err_map["msg"]:"";
	if (err_map["server_msg"]:"" != "")
	{
	    error = sformat ("%1\n%2", error, err_map["server_msg"]:"");
	}
	return error;
    }

    // this is a hack
    global define string GetFirstServer (string servers) ``{

	if (bind_pass == nil && servers == "")
	{
	    y2milestone ("--- server not read yet or empty, reading now");
	    servers	= ReadLdapConfEntry ("host", "");
	}

	list l_servers = splitstring (servers, " \t");
	string srv = l_servers[0]:"";
	return substring (srv, 0, find (srv, ":"));
    }

    // this is a hack
    global define integer GetFirstPort (string servers) ``{

	list l_servers = splitstring (servers, " \t");
	string srv = l_servers[0]:"";
	if (!issubstring (srv, ":"))
	    return default_port;
	string s_port = substring (srv, find (srv, ":") + 1);
	if (s_port == "")
	    return default_port;
	else return tointeger (s_port);
    }

    /**
     * Initializes LDAP agent
     */
    global define string LDAPInit () ``{

	// FIXME what if we have more servers? -> choose dialog?
	string ret = "";
	map args = $[
	    "hostname":	GetFirstServer (server),
	    "port":	GetFirstPort (server),
	    "version":	ldap_v2 ? 2 : 3,
	    "use_tls":	ldap_tls ? "yes" : "no"
	];
	boolean init = (boolean) SCR::Execute (.ldap, args);
	if (init == nil)
	{
	    // error message
	    ret = _("Unknown error. Perhaps 'yast2-ldap' is not available.");
	}
	else
	{
	    ldap_initialized	= init;
	    if (!init)
	    {
		ret = LDAPError();
	    }
	}
	return ret;
    }

    /**
     * Binds to LDAP server
     * @param pass password
     */
    global define string LDAPBind (string pass) ``{

        string ret = "";
	if (pass != nil)
	{
	    map args = $[];
	    if (!anonymous)
		args = $[ "bind_dn": bind_dn, "bind_pw": pass];
	    if (!(boolean)SCR::Execute (.ldap.bind, args))
		ret = LDAPError();
	    else
		bound = true;
	}
	return ret;
    }

    /**
     * Asks user for bind password to LDAP server
     * @param anonymous if anonymous access could be allowed
     * @return password
     */
    global define string GetLDAPPassword (boolean enable_anonymous) ``{

	UI::OpenDialog (`opt(`decorated),
        `VBox(
            `HSpacing(40),
            // password entering label
            `Password(`id(`pw), _("&Enter the Password for LDAP Server:")),
	    `Label (GetFirstServer (server)),
            `HBox(
              `PushButton (`id(`ok),`opt(`key_F10, `default),
		Label::OKButton()),
              // button label
	      `PushButton (`id(`anon), `opt(`key_F6), _("&Anonymous Access")),
              `PushButton (`id(`cancel),`opt(`key_F9), Label::CancelButton())
            ))
	);
	if (!enable_anonymous)
	    UI::ChangeWidget (`id(`anon), `Enabled, false);
	UI::SetFocus (`id(`pw));
	any ret = UI::UserInput();
	string pw = "";
	if (ret == `ok)
	{
	    pw = (string) UI::QueryWidget(`id(`pw), `Value);
	    anonymous = false;
	}
	else if (ret == `cancel)
	    pw = nil;
	else anonymous = true;
	UI::CloseDialog();
	return pw;
    }

    /**
     * Asks for LDAP password and tries to bind with it
     * @return password entered, nil on cancel
     */
    global define string LDAPAskAndBind (boolean enable_anonymous) ``{

	string pw = GetLDAPPassword (enable_anonymous);
        if (pw != nil)
	{
	    string ldap_msg = LDAPBind (pw);
	    while (pw != nil && ldap_msg != "")
	    {
		LDAPErrorMessage ("bind", ldap_msg);
		pw = GetLDAPPassword (enable_anonymous);
		ldap_msg = LDAPBind (pw);
	    }
	}
	return pw;
    }

    /**
     * Check if attribute allowes only single or multiple value
     * @param attr attribute name
     * @return answer
     */
    global define boolean SingleValued (string attr) ``{

	attr = tolower (attr);
	if (!haskey (attr_types, attr))
	{
	    map attr_type = (map)SCR::Read (.ldap.schema.at, $["name":attr]);
	    if (attr_type == nil)
		attr_type = $[];
	    attr_types [attr] = attr_type;
	}
	return attr_types [attr, "single"]:false;
    }

    /**
     * Gets the description of attribute (from schema)
     * @param attr attribute name
     * @return description
     */
    global define string AttributeDescription (string attr) ``{

	if (!haskey (attr_types, attr))
	{
	    map attr_type = (map)SCR::Read (.ldap.schema.at, $["name":attr]);
	    if (attr_type == nil)
		attr_type = $[];
	    attr_types [attr] = attr_type;
	}
	return attr_types [attr, "desc"]:"";
    }

    /**
     * Returns true if given object class exists in schema
     * @param class ObjectClass name
     */
    global define boolean ObjectClassExists (string class) ``{

	return (boolean)SCR::Read (.ldap.schema.oc.check, $["name":class]);
    }

    /**
     * Returns true if given object class is of 'structural' type
     * @param class ObjectClass name
     */
    global define boolean ObjectClassStructural (string class) ``{

	map object_class = (map)SCR::Read(.ldap.schema.oc, $["name":class]);
	return (object_class["kind"]:0 == 1);
    }


    /**
     * Returns allowed and required attributes of given object class
     * Read it from LDAP if it was not done yet.
     * @param string class name of object class
     * @return attribute names (list of strings)
     */
    global define list GetAllAttributes (string class) ``{

	class = tolower (class);
	if (!haskey (object_classes, class))
	{
	    map object_class = (map)SCR::Read(.ldap.schema.oc, $["name":class]);
	    if (object_class == nil)//TODO return from function?
		object_class = $[];
	    object_class ["all"] = union (object_class["may"]:[],
		object_class["must"]:[]);
	    // read attributes of superior classes
	    foreach (string sup_oc, object_class["sup"]:[], ``{
		list sup_all = GetAllAttributes (sup_oc);
		object_class ["all"] = union (object_class ["all"]:[], sup_all);
		object_class ["must"] = union (object_class ["must"]:[],
		    object_classes [sup_oc,"must"]:[]);
	    });
	    object_classes [class] = object_class;
	}
	return object_classes [class, "all"]:[];
    }

    /**
     * Returns required attributes of given object class
     * Read it from LDAP if it was not done yet.
     * @param string class name of object class
     * @return attribute names (list of strings)
     */
    global define list<string> GetRequiredAttributes (string class) ``{

	class = tolower (class);
	if (!haskey (object_classes, class))
	{
	    GetAllAttributes (class);
	}
	return object_classes [class, "must"]:[];
    }

    /**
     * Returns the list of all allowed and required attributes for each
     * object class, given in the list of object classes
     * @param classes list of object classes whose attributes we want
     * @return attribute names (list of strings)
     */
    global define list GetObjectAttributes (list classes) ``{

	list ret = [];
	foreach (string class, (list<string>) classes, ``{
	    ret = union (ret, GetAllAttributes (class));
	});
	return ret;
    }

    /**
     * For a given object, add all atributes this object is allowed to have
     * according to its "objectclass" value. Added attributes have empty values.
     * @param object map describing LDAP entry
     * @return updated map
     */
    global define map AddMissingAttributes (map object) ``{

	foreach (string class, object["objectclass"]:[], ``{
	    foreach (string attr, (list<string>) GetAllAttributes (class), ``{
		if (!haskey (object, tolower (attr)))
		    object = add (object, tolower (attr), []);
	    });
	});
	return object;
    }

    /**
     * Prepare agent for later schema queries
     * (agent reads schema to its internal structures)
     * @return error message
     */
    global define string InitSchema () ``{

	list schemas = (list)SCR::Read (.ldap.search, $[
	    "base_dn":  "",
	    "attrs":    [ "subschemasubentry" ],
	    "scope":    0, //0:base
	]);
	string schema_dn = schemas[0,"subschemasubentry",0]:"";
	if (schemas == nil || schema_dn == "")
	    return LDAPError();

	if (!(boolean)SCR::Execute (.ldap.schema, $[ "schema_dn": schema_dn ]))
	    return LDAPError();

	schema_initialized	= true;
	return "";
    }

    /**
     * In template object, convert the list of values
     * (where is in the form [ "a1=v1", "a2=v2"])
     * to map (in the form $[ "a1":"v1", "a2":"v2"]
     * @param templ original template map
     * @return updated template map
     */
    global define map ConvertDefaultValues (map templ) {

        map template = add (templ, "default_values", $[]);
	foreach (string value, templ["susedefaultvalue"]:[], {
	    list<string> lvalue	= splitstring (value, "=");
	    string at = tolower (lvalue[0]:"");
	    string v = substring (value, find (value, "=") + 1);
	    template ["default_values", at] = v;
	});
	return template;
    }

    /**
     * Read object templates from LDAP server
     * @return string error message
     */
    global define string ReadTemplates () ``{

	templates = $[];
	map all = (map)SCR::Read (.ldap.search, $[
	    "base_dn":	base_config_dn,
	    "filter":   "objectclass=suseobjecttemplate",
	    "attrs":	[],
	    "scope":	2,// sub: all templates under config DN
	    "map":	true,
	    "not_found_ok":	true,
	]);
	if (all == nil)
	{
	    return LDAPError();
	}
	// create a helper map of default values inside ...
	templates = mapmap (string dn, map templ, (map<string, map<string,any> >)all, ``{
	    map template = ConvertDefaultValues (templ);
	    template = AddMissingAttributes (template);
	    return $[dn : template];
	});
	return "";
    }

    /**
     * Read configuration moduels from LDAP server
     * @return string error message
     */
    global define string ReadConfigModules () ``{

	config_modules = $[];
	map modules = (map)SCR::Read (.ldap.search, $[
	    "base_dn":	base_config_dn,
	    "filter":   "objectclass=susemoduleconfiguration",
	    "attrs":	[],
	    "scope":	1, // one - deeper searches would have problems with
			   // constructing the dn
	    "map":	true,
	    "not_found_ok":	true,
	]);
	if (modules == nil)
	{
	    return LDAPError ();
	}
	config_modules = mapmap (string dn, map mod, (map<string, map<string,any> >) modules, ``{
	    return $[dn : AddMissingAttributes (mod)];
	});
	return "";
    }

    /**
     * Search for one entry (=base scope) in LDAP directory
     * @param dn DN of entry
     * @return map with entry values, empty map if nothing found, nil on error
     */
    global define map GetLDAPEntry (string dn) ``{

	if (!ldap_initialized)
	{
	    string msg = LDAPInit ();
	    if (msg != "")
	    {
		LDAPErrorMessage ("init", msg);
		return nil;
	    }
	}
	if (!schema_initialized)
	{
	    string msg = InitSchema ();
	    if (msg != "")
	    {
		LDAPErrorMessage ("schema", msg);
		return nil;
	    }
	}
	if (bind_pass == nil && !anonymous)
	{
	    bind_pass = LDAPAskAndBind (true);
	    if (bind_pass == nil)
		return nil;
	}
	list objects = (list)SCR::Read (.ldap.search, $[
	    "base_dn":	dn,
	    "attrs":	[],
	    "scope":	0, // only this one
	    "not_found_ok": true
	]);
	if (objects == nil)
	{
	    LDAPErrorMessage ("read", LDAPError());
	    return nil;
	}
	return objects[0]:$[];
    }

    /**
     * Check for existence of parent object of given DN in LDAP tree
     * return the answer
     */
    global define boolean ParentExists (string dn) ``{

	string parent = substring (dn, find (dn,",")+1);
	map object = GetLDAPEntry (parent);
	if (object == nil)
	    return false;
	if (object == $[])
	{
	    if (!use_gui)
	    {
		y2error ("A direct parent for DN %1 does not exist in the LDAP directory. The object with the selected DN cannot be created.", dn);
		return false;
	    }
	    // error message, %1 is DN
	    Popup::Error (sformat (_("A direct parent for DN '%1' 
does not exist in the LDAP directory.
The object with the selected DN cannot be created.
"), dn));
	    return false;
	}
	return true;
    }

    /**
     * Return main configuration object DN
     */
    global define string GetMainConfigDN () ``{

	return base_config_dn;
    }

    /**
     * Return the map of configuration modules (new copy)
     * (in the form $[ DN: $[ map_of_one_module] ])
     */
    global define map GetConfigModules () ``{

	return (map) eval (config_modules);
    }

    /**
     * Return the map of templates (new copy)
     */
    global define map GetTemplates () ``{

	return (map) eval (templates);
    }

    /**
     * Return list of default object classes for user or group
     * There is fixed list here, it is not saved anywhere (only in default
     * users plugin for LDAP objects)
     * @param template used for differ if we need user or group list
     */
    global define list GetDefaultObjectClasses (map template) {

	list ocs = maplist (string c,template["objectclass"]:[],``(tolower(c)));

	if (contains (ocs, "susegrouptemplate"))
	{
	    return ["top", "posixgroup", "groupofnames"];
	    // TODO sometimes there is groupofuniquenames...
	}
	else if (contains (ocs, "suseusertemplate"))
	{
	    return ["top","posixaccount","shadowaccount", "inetorgperson"];
	}
	return [];
    }

    /**
     * Creates default new map for a new object template
     * @param cn cn of new template
     * @param classes object classes of the object the template will belong to
     * @return template map
     */
    global define map CreateTemplate (string cn, list<string> classes) ``{

	map obj = $[
	    "cn":		[ cn ],
	    "modified":		"added"
	];
	classes = maplist (string c, classes, ``(tolower (c)));
	if (contains (classes, "suseuserconfiguration"))
	{
	    obj = union (obj, new_objects ["suseusertemplate"]:$[]);
	}
	else if (contains (classes, "susegroupconfiguration"))
	{
	    obj = union (obj, new_objects ["susegrouptemplate"]:$[]);
	}
	else
	{
	    obj["objectclass"] = [ "top", "suseobjecttemplate"];
	}

	obj = ConvertDefaultValues (obj);
	return AddMissingAttributes (obj);
    }

    /**
     * Creates default new map for new configuration object
     * @param class additional objectclass of new module (e.g.userConfiguration)
     * @return new module map
     */
    global define map<string,any> CreateModule (string cn, string class) ``{

	class = tolower (class);
	map obj = $[
	    "cn":		[ cn ],
	    "objectclass":	add (["top", "susemoduleconfiguration"], class),
	    "modified":		"added"
	];
	// create some good defaults
	obj = union (obj, new_objects [class]:$[]);
	list templs = [];
	string templ_cn = "";
	string default_base = "";
	if (class == "suseuserconfiguration")
	{
	    foreach (string dn, map t, (map<string,map<string,any> >)templates, ``{
		list cls = maplist(string c,t["objectclass"]:[],``(tolower(c)));
		if (contains (cls, "suseusertemplate"))
		    templs = add (templs, dn);
	    });
	    if (templs == [])
		templ_cn = "usertemplate";
	    default_base = sformat ("ou=people,%1", domain);
	}
	if (class == "susegroupconfiguration")
	{
	    foreach (string dn, map t, (map<string,map<string,any> >)templates, ``{
		list cls = maplist(string c,t["objectclass"]:[],``(tolower(c)));
		if (contains (cls, "susegrouptemplate"))
		    templs = add (templs, dn);
	    });
	    if (templs == [])
		templ_cn = "grouptemplate";
	    default_base = sformat ("ou=group,%1", domain);
	}
	// create proposal for defaultTemplate DN
	if (templ_cn != "")
	{
	    string tdn = sformat ("cn=%1,%2", templ_cn, base_config_dn);
	    integer i = 0;
	    while (size (GetLDAPEntry (tdn)) > 0)
	    {
		tdn = sformat ("cn=%1%2,%3", templ_cn, i, base_config_dn);
		i = i + 1;
	    }
	    templs = [ tdn ];
	}
	obj ["susedefaulttemplate"] = templs;
	obj ["susedefaultbase"] = [ default_base ];
	return (map<string,any>) AddMissingAttributes (obj);
    }

    /**
     * Searches for DN's of all objects defined by filter in given base ("sub")
     * @param base search base
     * @param search_filter if filter is empty, "objectclass=*" is used
     * @return list of DN's (list of strings)
     */
    global define list<string> ReadDN (string base, string search_filter) ``{

	list<string> all = (list<string>)SCR::Read (.ldap.search, $[
	    "base_dn":	base,
	    "filter":   search_filter,
	    "attrs":	["cn"], // not necessary, just not read all values
	    "attrsOnly":true,
	    "scope":	2,
	    "dn_only":	true,
	]);
	if (all == nil)
	{
	    LDAPErrorMessage ("read", LDAPError());
	    return [];
	}
	return all;
    }

    /**
     * Returns DN's of groups (objectclass=posixGroup) in given base
     * @param base LDAP search base
     * @return groups (list of strings)
     */
    global define list GetGroupsDN (string base) ``{

	if (groups_dn == [])
	    groups_dn = ReadDN (base, "objectclass=posixgroup");
	return groups_dn;
    }

    /**
     * Check if given DN exist and if it points to some template
     * @param dn
     * @return empty map if DN don't exist, template map if DN points
     *  to template object, nil if object with given DN is not template
     */
    global define map CheckTemplateDN (string dn) ``{

	map object = GetLDAPEntry (dn);
	if (object == nil)
	    return nil;
	if (object == $[])
	{
	    // OK, does not exist
	    return $[];
	}
	list cls = maplist (string c, object["objectclass"]:[],``(tolower (c)));
	if (contains (cls, "suseobjecttemplate"))
	{
	    // exists as a template -> return object
	    object = ConvertDefaultValues (object);
	    object ["modified"] = "edited";
	    return AddMissingAttributes (object);
	}
	else
	{
	    // error message
	    Popup::Error (_("Object with selected DN exists, but it is not a template object.
Select another one.
"));
	    return nil;
	}
    }

    /**
     * Save the edited map of configuration modules to global map
     */
    global define boolean CommitConfigModules (map modules) ``{

	foreach (string dn, map modmap, (map<string,map<string,any> >)modules, ``{

	    if (!haskey (config_modules, dn))
	    {
		config_modules [dn] = eval (modmap);
		ldap_modified = true;
		return;
	    }
	    // 'val' can be list (most time), map (default_values), string
	    foreach (string attr, any val, (map<string,any>) modmap, ``{
		if (config_modules [dn, attr]:nil != val)
		{
		    config_modules [dn, attr] = val;
		    if (!haskey (modmap, "modified"))
			config_modules [dn, "modified"] = "edited";
		    ldap_modified = true;
		    y2debug ("modified value: %1", val);
		}
	    });
	});
	return true;
    }

    /**
     * Save the edited map of templates to global map
     */
    global define boolean CommitTemplates (map templs) ``{

	foreach (string dn, map template, (map<string,map<string,any> >)templs, ``{

	    if (!haskey (templates, dn))
	    {
		// dn changed
		templates [dn] = eval (template);
		ldap_modified = true;
		return;
	    }
	    // 'val' can be list (most time), map (default_values), string
	    foreach (string attr, any val, (map<string,any>) template, ``{
		if (templates [dn, attr]:nil != val)
		{
		    templates [dn, attr] = val;
		    if (!haskey (template, "modified"))
			templates [dn, "modified"] = "edited";
		    ldap_modified = true;
		    y2debug ("modified value: %1", val);
		}
	    });
	});
	return true;
    }

    /**
     * Writes map of objects to LDAP
     * @param objects map of objects to write. It is in the form:
     * $[ DN: (map) attribute_values]
     * @example TODO
     * @return error map (empty on success)
     */
    global define map WriteToLDAP (map objects) ``{

	map ret = $[];
	foreach (string dn, map object, (map<string,map<string,any> >)objects, ``{

	    if (ret != $[])
		return;
	    string action = object["modified"]:"";
	    if (action != "")
		object = remove (object, "modified");
	    else return;

	    if (haskey (object, "default_values"))
	    {
		object["susedefaultvalue"] = maplist (string key, string val,
		    (map<string,string>) object["default_values"]:$[], ``{
		    return sformat ("%1=%2", key, val);
		});
		object = remove (object, "default_values");
	    }
	    if (action == "added")
	    {
		if (!(boolean)SCR::Write (.ldap.add, $[ "dn": dn ], object))
		    ret = LDAPErrorMap();
	    }
	    if (action == "edited")
	    {
		if (!(boolean)SCR::Write (.ldap.modify, $[
		    "dn":		dn,
		    "check_attrs":	true ], object))
		    ret = LDAPErrorMap();
	    }
	    if (action == "renamed")
	    {
		map arg_map	= $[
		    "dn"		: object["old_dn"]:dn,
		    "check_attrs"	: true
		];
		if (tolower (dn) != tolower (object["old_dn"]:dn))
		{
		    arg_map["new_dn"]		= dn;
		    arg_map["deleteOldRDN"]	= true;
		    arg_map["subtree"]		= true;
		}
		if (haskey (object, "old_dn"))
		    object  = remove (object, "old_dn");
		if (!(boolean)SCR::Write (.ldap.modify, arg_map, object))
		    ret = LDAPErrorMap();
	    }
	    if (action == "deleted")
	    {
		if (object ["old_dn"]:dn != dn)
		    dn = object ["old_dn"]:dn;
		if (!(boolean)SCR::Write (.ldap.delete, $[ "dn": dn ]))
		    ret = LDAPErrorMap();
	    }
	});
	return ret;
    }

    /**
     * Writes map of objects to LDAP. Ask for password, when needed and
     * shows the error message when necessary.
     * @return success
     */
    global define boolean WriteLDAP (map objects) ``{

	map error = $[];
	if (anonymous || bind_pass == nil)
	{
	    bind_pass = LDAPAskAndBind (false);
	}
	// nil means "canceled"
	if (bind_pass != nil)
	{
	    error = WriteToLDAP (objects);
	    if (error != $[])
	    {
		string msg	= error["msg"]:"";
		if (error["server_msg"]:"" != "")
		    msg		= msg + "\n" + error["server_msg"]:"";
		LDAPErrorMessage ("write", msg);
	    }
	}
	return (error == $[] && bind_pass != nil);
    }

    /**
     * Modify also /etc/openldap/ldap.conf for the use of
     * ldap client utilities (like ldapsearch)
     * @return modified?
     */
    global define boolean WriteOpenLdapConf () ``{

	boolean write_openldap_conf = openldap_modified;

	if (!Package::Installed ("openldap2-client"))
	    return false;

	map out = (map)SCR::Execute(.target.bash_output,
	    "/bin/rpm -V openldap2-client");

	list open_host	= (list) SCR::Read
	    (.etc.ldap_conf.v."/etc/openldap/ldap.conf".host);
	list open_base	= (list) SCR::Read
	    (.etc.ldap_conf.v."/etc/openldap/ldap.conf".base);

	// if the config file was not modified by user yet
	if (!issubstring(out["stdout"]:"", "/etc/openldap/ldap.conf"))
	    write_openldap_conf = true;
	// if there are same values as in /etc/ldap.conf
	else if (old_server == open_host[0]:"" && old_domain == open_base[0]:"")
	{
	    write_openldap_conf = true;
	}

	if (write_openldap_conf)
	{
	    // update ldap.conf
	    SCR::Write (.etc.ldap_conf.v."/etc/openldap/ldap.conf".host,
		[server]);
	    SCR::Write(.etc.ldap_conf.v."/etc/openldap/ldap.conf".base,
		[domain]);
	    if (ldap_tls)
	    {
		SCR::Write(.etc.ldap_conf.v."/etc/openldap/ldap.conf".TLS_REQCERT, ["allow"]);
	    }
	    y2milestone ("file /etc/openldap/ldap.conf was modified");
	}
	return write_openldap_conf;
    }


    /**
     * If a file does not + entry, add it.
     * @param	is login allowed?
     * @return	success?
     */
    global define boolean WritePlusLine (boolean login) ``{

	string file	= "/etc/passwd";
	string what	= "+::::::";
	if (!login)
	    what	= "+::::::/sbin/nologin";

	if (!passwd_read)
	{
	    if (!(boolean)SCR::Execute(.passwd.init,$["base_directory":"/etc"]))
	    {
		y2error ("error: %1", SCR::Read (.passwd.error));
		return false;
	    }
	    else
	    {
		passwd_read		= true;
		plus_lines_passwd	=
		    (list<string>) SCR::Read (.passwd.passwd.pluslines);
	    }
	}

	list<string> plus_lines	= plus_lines_passwd;

	if (!contains (plus_lines, what))
	{
	    plus_lines = maplist (string plus_line, plus_lines,``{
		if (!login && plus_line == "+::::::")
		    return what;
		if (login && issubstring (plus_line, ":/sbin/nologin"))
		    return what;
		return plus_line;
	    });
	    if (!contains (plus_lines, what))
		plus_lines	= add (plus_lines, what);

	    if ((boolean) SCR::Write (.passwd.passwd.pluslines, plus_lines))
	    {
		SCR::Execute (.target.bash,
		    sformat("/bin/cp %1 %1.YaST2save",file));
		// empty map map as a parameter means "use data you have read"
		if (!SCR::Write (.passwd.users, $[]))
		{
		    Report::Error (Message::ErrorWritingFile (file));
		    return false;
		}
	    }
	}

	file	= "/etc/group";
	what	= "+:::";
	plus_lines	= (list<string>) SCR::Read (.passwd.group.pluslines);

	if (!contains (plus_lines, what))
	{
	    plus_lines	= add (plus_lines, what);

	    if ((boolean) SCR::Write (.passwd.group.pluslines, plus_lines))
	    {
		SCR::Execute (.target.bash,
		    sformat("/bin/cp %1 %1.YaST2save",file));
		// empty map map as a parameter means "use data you have read"
		if (!SCR::Write (.passwd.groups, $[]))
		{
		    Report::Error (Message::ErrorWritingFile (file));
		    return false;
		}
	    }
	}
    }

    /**
     * create the default objects for users and groups
     */
    define boolean CreateDefaultLDAPConfiguration () ``{

	string msg	= "";
	if (!ldap_initialized)
	{
	    msg = LDAPInit ();
	    if (msg != "")
	    {
		LDAPErrorMessage ("init", msg);
		return false;
	    }
	}
	if (!schema_initialized)
	{
	    msg = InitSchema ();
	    if (msg != "")
	    {
	        LDAPErrorMessage ("schema", msg);
	    }
	}
	if (bind_pass != nil && !bound)
	{
	    msg = LDAPBind (bind_pass);
	    if (msg != "")
	    {
	        LDAPErrorMessage ("bind", msg);
	        bind_pass	= nil;
	    }
	}
	// create base configuration object
	map object = GetLDAPEntry (base_config_dn);
	if (object == nil)
	    return false;
	if (object == $[])
	{
	    if (ParentExists (base_config_dn))
	    {
	        if (!WriteLDAP ( $[ base_config_dn : $[
		    "objectclass"	: [ "top", "organizationalunit"],
		    "modified"		: "added",
		    "ou"		: get_cn (base_config_dn)]]))
	        {
		   y2error ("problem");
	        }
	    }
	    //TODO fail?
	}

	map modules		= $[];
	map templs		= $[];
	string user_dn	= get_dn ("userconfiguration");
	string group_dn	= get_dn ("groupconfiguration");

	if (config_modules == $[])
	{
	    ReadConfigModules ();
	}

	// check which objects already exist...
	foreach (string dn, map m, (map<string,map>) config_modules, ``{
	    list cl = maplist (string c, m["objectclass"]:[],``(tolower(c)));
	    if (contains (cl, "suseuserconfiguration"))
	        user_dn = dn;
	    if (contains (cl, "susegroupconfiguration"))
	        group_dn = dn;
	});

	// create user configuration object
	if (GetLDAPEntry (user_dn) == $[])
	{
	    modules [user_dn] = Ldap::CreateModule (
	        get_cn (user_dn), "suseuserconfiguration");
	}

	// create group configuration object
	if (GetLDAPEntry (group_dn) == $[])
	{
	    modules [group_dn] = Ldap::CreateModule (
	        get_cn (group_dn), "susegroupconfiguration");
	}

	CommitConfigModules (modules);
	modules = GetConfigModules ();
	boolean update_modules	= false;

	// create user template...
	string template_dn	=
	    get_string (modules[user_dn]:$[], "susedefaulttemplate");
	if (modules [user_dn,"susedefaulttemplate"]:[] == [])
	{
	    template_dn	= "cn=usertemplate," + base_config_dn;
	    modules [user_dn,"susedefaulttemplate"]	= [ template_dn ];
	    update_modules		= true;
	}

	if (GetLDAPEntry (template_dn) == $[])
	{
	    templs [template_dn]	= Ldap::CreateTemplate (
	        get_cn (template_dn), ["suseuserconfiguration"]);
	}

	// group template...
	template_dn	=
	    get_string (modules[group_dn]:$[], "susedefaulttemplate");
	if (modules [group_dn,"susedefaulttemplate"]:[] == [])
	{
	    template_dn	= "cn=grouptemplate," + base_config_dn;
	    modules [group_dn,"susedefaulttemplate"] = [ template_dn ];
	    update_modules		= true;
	}

	if (GetLDAPEntry (template_dn) == $[])
	{
	    templs [template_dn]	= Ldap::CreateTemplate (
	        get_cn (template_dn), ["susegroupconfiguration"]);
	}

	if (update_modules)
	    CommitConfigModules (modules);
	CommitTemplates (templs);
	return true;
    }

    /**
     * Saves LDAP configuration.
     * @param abort block for abort
     * @return symbol
     */
    global define symbol Write (block<boolean> abort) {

	// progress caption
	string caption = _("Writing LDAP Configuration...");
	integer no_of_steps = 4;

	Progress::New (caption, " ", no_of_steps, [
	    // progress stage label
	    _("Stop services"),
	    // progress stage label
	    _("Update configuration files"),
	    // progress stage label
	    _("Start services"),
	    // progress stage label
	    _("Update configuration in LDAP directory"),
           ], [
	    // progress step label
	    _("Stopping services..."),
	    // progress step label
	    _("Updating configuration files..."),
	    // progress step label
	    _("Starting services..."),
	    // progress step label
	    _("Updating configuration in LDAP directory..."),
	    // final progress step label
	    _("Finished") ],
	    "" );

	// -------------------- stop services
	Progress::NextStage ();
	if (eval (abort))
	    return `abort;

	boolean ypbind_running = false;

	if (!write_only)
	{
	    ypbind_running = (Service::Status ("ypbind") == 0);
	    Service::Stop ("ypbind");
	}
	else if (write_only && Mode::autoinst () )
	{
	    // Read existing nsswitch in autoinstallation mode
	    foreach (string db,
		["passwd","group","passwd_compat","group_compat"],``{
		nsswitch[db]	= Nsswitch::ReadDb (db);
	    });
	}

	// -------------------- update config files
	Progress::NextStage ();
	if (eval(abort))
	    return `abort;

	if (modified)
	{
	    // update ldap.conf
	    WriteLdapConfEntry ("host", server);
	    WriteLdapConfEntry ("base", domain);

	    if (member_attribute != old_member_attribute)
	    {
		WriteLdapConfEntries ("nss_map_attribute",
		    ["uniqueMember", member_attribute ]);
	    }

	    WriteOpenLdapConf();

	    if (ldap_v2)
		WriteLdapConfEntry ("ldap_version", "2");
	    else
		WriteLdapConfEntry ("ldap_version", "3");

	    if (ldap_tls)
		WriteLdapConfEntry ("ssl", "start_tls");
	    else
		WriteLdapConfEntry ("ssl", "no");

	}
	if (start) // ldap used for authentocation
	{

	    WriteLdapConfEntry ("pam_password", pam_password);

	    // see bug #37665
	    AddLdapConfEntry ("pam_filter", "objectclass=posixAccount");

	    // save the user and group bases
	    user_base = domain;
	    group_base = domain;

	    // if nss_base_* were equal to base DN, update them
	    if (nss_base_passwd != "" && nss_base_passwd == old_domain)
		nss_base_passwd	= domain;
	    if (nss_base_shadow != "" && nss_base_shadow == old_domain)
		nss_base_shadow	= domain;
	    if (nss_base_group != "" && nss_base_group == old_domain)
		nss_base_group	= domain;

	    WriteLdapConfEntry ("nss_base_passwd",
		(nss_base_passwd != "")? nss_base_passwd : user_base);
	    WriteLdapConfEntry ("nss_base_shadow",
		(nss_base_shadow != "")? nss_base_shadow : user_base);
	    WriteLdapConfEntry ("nss_base_group",
		(nss_base_group != "")? nss_base_group : user_base);

	    // pam settigs
	    foreach (string type, [ "account", "auth", "password" ], {
		PamSettings::AddValue ("pam_unix2", type, "use_ldap");

		// prefer kerberos authentication (see #49894)
		if (krb_authentication == true)
		{
		    PamSettings::RemoveValue ("unix2", type, "use_krb5");
		    if (contains (auth_modules, "krb5afs"))
		    {
			PamSettings::RemoveValue ("unix2", type,"call_modules");
			PamSettings::AddValue ("unix2", type, "call_modules=" +
			    mergestring (auth_modules,","));
		    }
		    else
		    {
			PamSettings::AddValue ("unix2", type, "use_krb5");
		    }
		}
	    });

	    // modify sources in /etc/nsswitch.conf
	    foreach (string db, [ "passwd", "group" ], ``{
		string new_db		= db+"_compat";
		nsswitch [new_db]	= union (nsswitch[new_db]:[], ["ldap"]);
		Nsswitch::WriteDb (db, ["compat"]);
		Nsswitch::WriteDb (new_db, nsswitch[new_db]:["ldap"]);
	    });

	    Nsswitch::Write ();
	    Autologin::Write (write_only);
	}
	else // ldap is not used
	{
	    //TODO: first check, if nss needs to be updated...
	    foreach (string db, [ "passwd", "group" ], ``{
		string new_db		= db+"_compat";
		nsswitch [db]	= filter (
		    string v, nsswitch[db]:[], ``(v != "ldap"));
		if (nsswitch[db]:[] == [])
		    nsswitch [db]	= ["compat"];
		nsswitch [new_db]	= filter (
		    string v, nsswitch[new_db]:[], ``(v != "ldap"));
		Nsswitch::WriteDb (db, nsswitch[db]:["compat"]);
		Nsswitch::WriteDb (new_db, nsswitch[new_db]:[]);
	    });

	    Nsswitch::Write ();

	    foreach (string type, [ "account", "auth", "password" ], ``{
		PamSettings::RemoveValue ("pam_unix2", type, "use_ldap");
	    });
	}

	// write the changes in /etc/ldap.conf and /etc/openldap/ldap.conf now
	if (!SCR::Write(.etc.ldap_conf, nil))
	{
	    y2error ("error writing ldap.conf file");
	}
	if (Stage::cont ())
	{
	    SCR::UnmountAgent (.etc.ldap_conf);
	}

	// write changes to /etc/security/pam_unix2.conf now
	PamSettings::Write (false);

	// write sysconfig values
	SCR::Write (.sysconfig.ldap.FILE_SERVER, file_server? "yes": "no");

	SCR::Write (.sysconfig.ldap.BASE_CONFIG_DN, base_config_dn);

	SCR::Write (.sysconfig.ldap.BIND_DN, bind_dn);

	// write the changes in /etc/sysconfig/ldap now
	if (!SCR::Write (.sysconfig.ldap, nil))
	{
	    y2error ("error writing /etc/sysconfig/ldap");
	}

	if (_autofs_allowed)
	{
	    if (Nsswitch::WriteAutofs (start && _start_autofs, "ldap"))
	    {
		if (_start_autofs)
		{
		    Service::Adjust ("autofs", "enable");
		}
		else
		{
		    Service::Adjust ("autofs", "disable");
		}
	    }
	}

	WritePlusLine (login_enabled);

	// -------------------- start services
	Progress::NextStage ();
	if (eval(abort))
	    return `abort;

	if (!write_only)
	{
	    if (Service::Status ("nscd") == 0)
	    {
		Service::Restart ("nscd");
	    }

	    if (ypbind_running)
	    {
		Service::Restart ("ypbind");
	    }

	    if (restart_sshd)
	    {
		Service::Restart ("sshd");
	    }

	    if (_autofs_allowed)
	    {
		Service::Stop ("autofs");

		if (_start_autofs)
		{
		    Service::Start ("autofs");
		}
	    }
	}

	// -------------------- write settings to LDAP
	Progress::NextStage ();
	if (eval(abort))
	    return `abort;

	// ------------------------------ create the LDAP configuration (#40484)
	boolean ldap_ok = true;
	if (create_ldap && !Mode::autoinst ())
	{
	    ldap_ok	= CreateDefaultLDAPConfiguration ();
	}

	if (ldap_modified && ldap_ok)
	{
	    if (WriteLDAP (config_modules) && WriteLDAP (templates)) {
		ldap_modified	= false;
	    }
	}

	// final stage
	Progress::NextStage ();

	// unbind is done in agent destructor
	// ldap-client can be called more times from users module so we
	// will have to know it is necessary to bind again
	bound = false;
	if (modified)
	{
	    ldap_initialized    = false;
	    old_server          = server;
	    old_domain          = domain;
	}
	if (ldap_modified)
	{
	    config_modules  = $[];
            templates       = $[];
	}

	// now clear the initial default values, so next time Read will read
	// real values
	if (Stage::cont () && size (initial_defaults) > 0)
	{
	    if (start && ldap_ok &&
		domain == initial_defaults["ldap_domain"]:"" &&
		(server == initial_defaults["ldap_server"]:"" ||
		 server == "localhost" || server == "127.0.0.1"))
	    {
		initial_defaults_used	= true;
	    }
	    initial_defaults	= $[];
	}

	return `next;
    }

    /**
     * Check if base config DN belongs to some existing object and offer
     * creating it if necessary
     */
    global define boolean CheckBaseConfig (string dn) ``{

	map object = GetLDAPEntry (dn);
	if (object == nil)
	{
	    return false;
	}
	if (object == $[])
	{
	    // yes/no popup, %1 is value of DN
	    if (!use_gui || Popup::YesNo (sformat (_("No entry with DN '%1'
exists on the LDAP server. Create it now?
"), dn)))
	    {
		if (!ParentExists (dn))
		    return false;
		return WriteLDAP ( $[ dn : $[
		    "objectclass"	: [ "top", "organizationalunit"],
		    "modified"		: "added",
		    "ou"		: get_cn (dn)]]
		);
	    }
	    return false;
	}
	return true;
    }

    /**
     * Return list of architecture specific packages (derived from package list
     * given as parameter) merged with the packages in parameter
     */
    global define list<string> UpdatedArchPackages (list<string> packages) {

	list<string> ret	= packages;
	string arch		= Arch::architecture ();

	foreach (string package, packages, {
	    ret	= (list<string>) union (ret, arch_packages[package,arch]:[]);
	});
	return ret;
    }

    /**
     * Return needed packages and packages to be removed
     * during autoinstallation.
     * @return map of lists.
     */
    global  define map AutoPackages() ``{
      list<string> install_pkgs = UpdatedArchPackages (required_packages);
      list remove_pkgs = [];
      return ($["install": install_pkgs, "remove": remove_pkgs]);
    }

    /**
     * Set the value of bind_pass variable
     * @param pass new password valure
     */
    global define void SetBindPassword (string pass) ``{
	bind_pass = pass;
    }

    /**
     * Set the value of 'anonymous' variable (= bind without password)
     * @param anon new value
     */
    global define void SetAnonymous (boolean anon) ``{
	anonymous = anon;
    }

    /**
     * Set the value of 'use_gui' variable (= show error popups)
     * @param gui new value
     */
    global define void SetGUI (boolean gui) ``{
	use_gui	= gui;
    }

    /**
     * Set the value of restart_sshd (= restart sshd during write)
     */
    global define void RestartSSHD (boolean restart) {
	restart_sshd	= restart;
    }
}
