diff --git a/.arclint b/.arclint --- a/.arclint +++ b/.arclint @@ -19,11 +19,13 @@ "E131": "disabled", "E201": "disabled", "E202": "disabled", + "E221": "disabled", "E225": "disabled", "E231": "disabled", "E251": "disabled", "E261": "disabled", "E265": "disabled", + "E266": "disabled", "E302": "disabled", "E303": "disabled", "E402": "disabled", diff --git a/pykolab/auth/__init__.py b/pykolab/auth/__init__.py --- a/pykolab/auth/__init__.py +++ b/pykolab/auth/__init__.py @@ -49,7 +49,6 @@ Login is a simple list of username, password, service and, optionally, the realm. """ - if len(login) == 3: # The realm has not been specified. See if we know whether or not # to use virtual_domains, as this may be a cause for the realm not @@ -58,12 +57,12 @@ # TODO: Insert debug statements #if use_virtual_domains == "userid": - #print "# Derive domain from login[0]" + # print "# Derive domain from login[0]" #elif not use_virtual_domains: - #print "# Explicitly do not user virtual domains??" + # print "# Explicitly do not user virtual domains??" #else: - ## Do use virtual domains, derive domain from login[0] - #print "# Derive domain from login[0]" + # ## Do use virtual domains, derive domain from login[0] + # print "# Derive domain from login[0]" if len(login[0].split('@')) > 1: domain = login[0].split('@')[1] @@ -86,7 +85,7 @@ back to the primary domain specified by the configuration. """ - log.debug(_("Called for domain %r") % (domain), level=8) + log.debug(_("Called for domain %r") % (domain), level=5) if not self._auth == None: return @@ -99,14 +98,10 @@ section = 'kolab' domain = conf.get('kolab', 'primary_domain') else: + log.debug(_("Getting list of domains for %s ...") % (domain), level=5) self.list_domains(domain) section = domain - log.debug( - _("Using section %s and domain %s") % (section,domain), - level=8 - ) - if not self.domains == None and self.domains.has_key(domain): section = self.domains[domain] domain = self.domains[domain] @@ -116,13 +111,6 @@ level=8 ) - log.debug( - _("Connecting to Authentication backend for domain %s") % ( - domain - ), - level=8 - ) - if not conf.has_section(section): section = 'kolab' @@ -142,21 +130,32 @@ level=8 ) + _auth_mechanism = conf.get(section, 'auth_mechanism') + # Get the actual authentication and authorization backend. - if conf.get(section, 'auth_mechanism') == 'ldap': - log.debug(_("Starting LDAP..."), level=8) + if _auth_mechanism == 'ldap': + log.debug(_("Initializing LDAP..."), level=8) from pykolab.auth import ldap self._auth = ldap.LDAP(self.domain) - elif conf.get(section, 'auth_mechanism') == 'sql': + elif _auth_mechanism == 'sql': + log.debug(_("Initializing SQL..."), level=8) from pykolab.auth import sql self._auth = sql.SQL(self.domain) else: - log.debug(_("Starting LDAP..."), level=8) + log.debug(_("Fallback to LDAP. Initializing ..."), level=5) from pykolab.auth import ldap self._auth = ldap.LDAP(self.domain) + log.debug( + _("Connecting to Authentication %s backend for domain %s") % ( + _auth_mechanism, + domain + ), + level=5 + ) + self._auth.connect() def disconnect(self, domain=None): @@ -277,6 +276,8 @@ for secondary in secondaries: self.domains[secondary.lower()] = primary.lower() + log.debug(_("List of domains for %s is: %s") % (domain, ", ".join(self.domains)), level=8) + return self.domains def synchronize(self, mode=0, callback=None): @@ -291,6 +292,9 @@ def primary_domain_for_naming_context(self, domain): return self._auth._primary_domain_for_naming_context(domain) + def add_entry(self, domain, entry): + return self._auth._add_entry(entry) + def get_entry_attribute(self, domain, entry, attribute): return self._auth.get_entry_attribute(entry, attribute) diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py --- a/pykolab/auth/ldap/__init__.py +++ b/pykolab/auth/ldap/__init__.py @@ -1525,6 +1525,26 @@ log.debug(_("bind_priv() called but already bound"), level=8) return True + def _add_entry(self, entry): + """ + Add generic type of entry. + + Initialy created for setup-kolab to be able to add AD schema + by parsing Kolab AD schema ldif file + + entry - dictionary, same as search results + """ + self._bind() + + entry_dn, entry_attrs = entry + + log.debug(_("Entry DN: %s, with attributes: %s") % (entry_dn, entry_attrs), level=8) + + self.ldap.add_s( + entry_dn, + entry_attrs + ) + def _change_add_group(self, entry, change): """ An entry of type group was added. diff --git a/pykolab/setup/setup_ldap.py b/pykolab/setup/setup_ldap.py --- a/pykolab/setup/setup_ldap.py +++ b/pykolab/setup/setup_ldap.py @@ -22,6 +22,7 @@ import os import pwd import shutil +import socket import subprocess import tempfile import time @@ -35,6 +36,8 @@ from pykolab.constants import * from pykolab.translate import _ +from Cheetah.Template import Template + log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() @@ -95,9 +98,251 @@ def description(): return _("Setup LDAP.") +def get_answers(): + + answers = {} + + # Get cn=Directory Manger password + if conf.directory_manager_pwd is not None: + # password was set with command line options + answers['dirmgr_pass'] = conf.directory_manager_pwd + else: + # Ask for password + print >> sys.stderr, utils.multiline_message( + _(""" + Please supply a password for the LDAP Directory Manager + user, which is the administrator user you will be using + to at least initially log in to the Web Admin, and that + Kolab uses to perform administrative tasks. + """) + ) + + answers['dirmgr_pass'] = utils.ask_question( + _("Directory Manager password"), + default=utils.generate_password(), + password=True, + confirm=True + ) + + # Ask only if LDAP is DS + if not conf.with_openldap and not conf.with_ad: + # Get Dirsrv Admin server admin user pass + print >> sys.stderr, utils.multiline_message( + _(""" + Please supply a password for the LDAP administrator user + 'admin', used to login to the graphical console of 389 + Directory server. + """) + ) + + answers['admin_pass'] = utils.ask_question( + _("Administrator password"), + default=utils.generate_password(), + password=True, + confirm=True + ) + + + # Set users for DS LDAP + print >> sys.stderr, utils.multiline_message( + _(""" + Please choose the system user and group the service + should use to run under. These should be existing, + unprivileged, local system POSIX accounts with no shell. + """) + ) + + try: + pw = pwd.getpwnam("dirsrv") + except: + answers['userid'] = utils.ask_question(_("User"), default="nobody") + answers['group'] = utils.ask_question(_("Group"), default="nobody") + else: + answers['userid'] = utils.ask_question(_("User"), default="dirsrv") + answers['group'] = utils.ask_question(_("Group"), default="dirsrv") + + elif conf.with_ad and not conf.with_openldap: + # Get answers for Active Directory setup + print >> sys.stderr, utils.multiline_message( + _(""" + Specify basic Active Directory connection information. + """) + ) + + print >> sys.stderr, utils.multiline_message( + _(""" + Please provide Active Directory server URI. Setup will use it to load + Kolab schema into AD and then will configure Kolab to run with AD as + LDAP server. Schema can be loaded to schema master server only wherefore + this needs to be URI specifying AD schema master. You can change AD server + URI later, in kolab.conf file. + """) + ) + + answers['ad_server'] = utils.ask_question( + _("AD server URI"), + default="ldap://adserver.domain:389" + ) + + print >> sys.stderr, utils.multiline_message( + _(""" + Enter primary domain name which you would like to use for your Kolab installation. + """) + ) + + answers['domain'] = utils.ask_question( + _("Domain name to use"), + default=socket.getfqdn().split('.', 1)[1] + ) + + print >> sys.stderr, utils.multiline_message( + _(""" + Provide Root DN of your Active Directory LDAP tree. + """) + ) + + answers['rootdn'] = utils.ask_question( + _("Specify Active Directory root DN"), + default=utils.standard_root_dn(answers['domain']) + ) + + answers['bind_dn'] = utils.ask_question( + _("AD Administrator bind DN"), + default="CN=Administrator,CN=Users,%s" % answers['rootdn'] + ) + + return answers + +def ad_schema_import(*args, **kwargs): + import ldif + import time + from ldap import modlist + from ldap import dn + from StringIO import StringIO + + conf.ldap_uri = kwargs['uri'] + ad_domain = kwargs['domain'] + ad_bind_dn = kwargs['binddn'] + ad_bind_pw = kwargs['passwd'] + + # create LDAP connection + conn = Auth() + conn.connect() + + ad_schema_naming_context = conn.get_entry_attribute(ad_domain, "", "schemaNamingContext") + + log.debug("AD Schema Naming context is %s" % ad_schema_naming_context, level=8) + + # Find out AD schema master server. Only this server has writable copy of + # AD schema. First need to get fsmoRoleOwner attribute value. + ad_fsmo_attribute = conn.get_entry_attribute(ad_domain, ad_schema_naming_context, "fSMORoleOwner") + + log.debug(_("AD FSMO Role Owner entry is: %s") % ad_fsmo_attribute, level=8) + + # Now get parent of fsmoRoleOwner + if ad_fsmo_attribute is not None: + ad_fsmo_dn = dn.explode_dn(ad_fsmo_attribute) + ad_fsmo_parent = ','.join(ad_fsmo_dn[1:]) + + log.debug(_("AD FSMO entry parent is: %s") % ad_fsmo_parent, level=8) + + # Look for dNSHostName attribute of the parent + if ad_fsmo_parent is not None: + ad_schema_master = conn.get_entry_attribute(ad_domain, ad_fsmo_parent, "dNSHostName") + log.debug(_("AD schema master server is: %s") % ad_schema_master, level=8) + + ( + _ad_protocol, + _ad_server, + _ad_port, + _ad_base_dn, + _ad_attr, + _ad_scope, + _ad_filter + ) = utils.parse_ldap_uri(conf.ldap_uri) + + if not _ad_server == ad_schema_master: + log.debug(_("AD schema master %s is not the same as %s.") % (ad_schema_master, _ad_server), level=5) + log.error(_("Cannot install Kolab schema on AD server which is not schema master")) + sys.exit(1) + + template_file = None + if os.path.isfile('/etc/kolab/templates/kolab-ad-schema.tpl'): + template_file = '/etc/kolab/templates/kolab-ad-schema.tpl' + elif os.path.isfile('/usr/share/kolab/templates/kolab-ad-schema.tpl'): + template_file = '/usr/share/kolab/templates/kolab-ad-schema.tpl' + elif os.path.isfile(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'kolab-ad-schema.tpl' ))): + template_file = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'kolab-ad-schema.tpl' )) + + if not template_file == None: + log.debug(_("Using template file %r") % (template_file), level=8) + fp = open(template_file, 'r') + template_definition = fp.read() + fp.close() + + schema_template = Template(template_definition, searchList={'AD_SCHEMA_NAMING_CONTEXT': ad_schema_naming_context}) + log.debug( + _("Successfully compiled template %r, ready to add schema to AD server") % (template_file), + level=8 + ) + + schema_ldif = StringIO(schema_template.__str__()) + + parser = ldif.LDIFRecordList(schema_ldif) + parser.parse() + + # If silent install or install without asking questions + # when no need to ask confirmation + if kwargs['confirm']: + + print >> sys.stderr, utils.multiline_message( + _(""" + Setup is going to add Kolab schema elements to + Active Directory. This is ireversable operation. + """) + ) + + confirmed = utils.ask_confirmation( + _("Modify Active Directory schema?"), + default="n" + ) + + else: + # this is silent install + confirmed = True + + if confirmed: + for dn, attributes in parser.all_records: + log.debug("Adding DN: %s" % dn, level=5) + try: + add_attributes = modlist.addModlist(attributes) + conn.add_entry(None, (dn, add_attributes)) + except ldap.ALREADY_EXISTS: + log.warning(_("Schema element already exist: %s") % dn) + continue + except ldap.NO_SUCH_ATTRIBUTE: + log.warning(_("Could not add %s schema element. Force update and retry.") % dn) + try: + conn.set_entry_attribute(None, "", "schemaUpdateNow", "1") + time.sleep(2) + conn.add_entry(None, (dn, add_attributes)) + except: + log.error(_("Failed to add %s Kolab schema element.") % dn) + sys.exit(1) + except Exception, errorMsg: + log.error(_("Could not add schema element: %s") % dn) + log.error(_("Error: %s") % errorMsg ) + sys.exit(1) + else: + log.error(_("Kolab schema must be installed before you continue.")) + sys.exit(1) + def execute(*args, **kw): ask_questions = True + log.error("_________ %s ________" % conf.config_file) + log.error("_________ %s ________" % conf.defaults.config_file) + if not conf.config_file == conf.defaults.config_file: ask_questions = False @@ -108,7 +353,7 @@ _input = {} if conf.with_openldap and not conf.with_ad: - + conf.command_set('ldap', 'type', 'openldap') conf.command_set('ldap', 'unique_attribute', 'entryuuid') fp = open(conf.defaults.config_file, "w+") @@ -118,14 +363,50 @@ return elif conf.with_ad and not conf.with_openldap: - conf.command_set('ldap', 'auth_attributes', 'samaccountname') - conf.command_set('ldap', 'modifytimestamp_format', '%%Y%%m%%d%%H%%M%%S.0Z') - conf.command_set('ldap', 'unique_attribute', 'userprincipalname') + + if ask_questions: + _input = get_answers() + else: + if conf.fqdn is None: + print >> sys.stderr, utils.multiline_message( + _(""" + FQDN not specified. Either use config file or + --fqdn option to specify domain to use. + """) + ) + sys.exit(1) + else: + _input['domain'] = conf.fqdn + _input['rootdn'] = utils.standard_root_dn(_input['domain']) + _input['bind_dn'] = conf.get('ldap', 'bind_dn') + _input['dirmgr_pass'] = conf.get('ldap', 'bind_pw') + _input['ad_server'] = conf.get('ldap', 'ldap_uri') + + conf.command_set('ldap', 'type', 'ad') + conf.command_set('ldap', 'auth_attributes', 'mail, kolabAlias, uid, samaccountname') + conf.command_set('ldap', 'modifytimestamp_format', '%Y%m%d%H%M%S.0Z') + conf.command_set('ldap', 'unique_attribute', 'objectguid') # TODO: These attributes need to be checked conf.command_set('ldap', 'mail_attributes', 'mail') - conf.command_set('ldap', 'mailserver_attributes', 'mailhost') - conf.command_set('ldap', 'quota_attribute', 'mailquota') + conf.command_set('ldap', 'mailserver_attributes', 'kolabMailhost') + conf.command_set('ldap', 'quota_attribute', 'kolabMailquota') + + # TODO: These attributes need to be checked + conf.command_set('ldap', 'ldap_uri', _input['ad_server']) + conf.command_set('ldap', 'bind_dn', _input['bind_dn']) + conf.command_set('ldap', 'bind_pw', _input['dirmgr_pass']) + conf.command_set('ldap', 'service_bind_dn', _input['bind_dn']) + conf.command_set('ldap', 'service_bind_pw', _input['dirmgr_pass']) + conf.command_set('ldap', 'primary_domain', _input['domain']) + + ad_schema_import( + domain=_input['domain'], + uri=_input['ad_server'], + binddn=_input['bind_dn'], + passwd=_input['dirmgr_pass'], + confirm=ask_questions + ) return @@ -158,57 +439,7 @@ _input = {} if ask_questions: - print >> sys.stderr, utils.multiline_message( - _(""" - Please supply a password for the LDAP administrator user - 'admin', used to login to the graphical console of 389 - Directory server. - """) - ) - - _input['admin_pass'] = utils.ask_question( - _("Administrator password"), - default=utils.generate_password(), - password=True, - confirm=True - ) - - if conf.directory_manager_pwd is not None: - _input['dirmgr_pass'] = conf.directory_manager_pwd - else: - print >> sys.stderr, utils.multiline_message( - _(""" - Please supply a password for the LDAP Directory Manager - user, which is the administrator user you will be using - to at least initially log in to the Web Admin, and that - Kolab uses to perform administrative tasks. - """) - ) - - _input['dirmgr_pass'] = utils.ask_question( - _("Directory Manager password"), - default=utils.generate_password(), - password=True, - confirm=True - ) - - print >> sys.stderr, utils.multiline_message( - _(""" - Please choose the system user and group the service - should use to run under. These should be existing, - unprivileged, local system POSIX accounts with no shell. - """) - ) - - try: - pw = pwd.getpwnam("dirsrv") - except: - _input['userid'] = utils.ask_question(_("User"), default="nobody") - _input['group'] = utils.ask_question(_("Group"), default="nobody") - else: - _input['userid'] = utils.ask_question(_("User"), default="dirsrv") - _input['group'] = utils.ask_question(_("Group"), default="dirsrv") - + _input = get_answers() else: _input['admin_pass'] = conf.get('ldap', 'bind_pw') _input['dirmgr_pass'] = conf.get('ldap', 'bind_pw')