diff options
-rw-r--r-- | pykolab/auth/__init__.py | 128 | ||||
-rw-r--r-- | pykolab/auth/ldap/__init__.py | 406 |
2 files changed, 450 insertions, 84 deletions
diff --git a/pykolab/auth/__init__.py b/pykolab/auth/__init__.py index f744cc1..503867b 100644 --- a/pykolab/auth/__init__.py +++ b/pykolab/auth/__init__.py @@ -16,8 +16,11 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # -from pykolab.conf import Conf +import logging +import os +import time +from pykolab.conf import Conf from pykolab.translate import _ class Auth(object): @@ -30,30 +33,113 @@ class Auth(object): Initialize the authentication class. """ self.conf = conf - if hasattr(self.conf, "log"): - self.log = self.conf.log + self.log = logging.getLogger('pykolab') + + self._auth = {} + + def authenticate(self, login): + # Login is a list of authentication credentials: + # 0: username + # 1: password + # 2: service + # 3: realm, optional + + if len(login) == 3: + # Realm not set + use_virtual_domains = self.conf.get('imap', 'virtual_domains', quiet=True) + if use_virtual_domains == "userid": + print "# Derive domain from login[0]" + elif not use_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]" + + if len(login[0].split('@')) > 1: + domain = login[0].split('@')[1] + else: + domain = self.conf.get("kolab", "primary_domain") + + # realm overrides domain + if len(login) == 4: + domain = login[3] - self._auth = None + self.connect(domain) - def _connect(self): - if not self._auth == None: + retval = self._auth[domain]._authenticate(login, domain) + + return retval + + def connect(self, domain=None): + """ + Connect to the domain authentication backend using domain, or fall + back to the primary domain specified by the configuration. + """ + + if domain == None: + section = 'kolab' + domain = self.conf.get('kolab', 'primary_domain') + else: + section = domain + + if self._auth.has_key(section) and not self._auth[section] == None: return - if self.conf.get('kolab', 'auth_mechanism') == 'ldap': - try: - from pykolab.auth import ldap - except: - if hasattr(self, "log"): - self.log.error(_("Failure to import authentication layer %s," + - " please verify module dependencies have been installed") % "ldap") - self._auth = ldap.LDAP(self.conf) - - def users(self): - self._connect() - users = self._auth._kolab_users() + #print "Connecting to Authentication backend for domain %s" %(domain) + + if not self.conf.has_section(section): + section = 'kolab' + + if self.conf.get(section, 'auth_mechanism') == 'ldap': + from pykolab.auth import ldap + self._auth[domain] = ldap.LDAP(self.conf) + elif self.conf.get(section, 'auth_mechanism') == 'sql': + from pykolab.auth import sql + self._auth[domain] = sql.SQL(self.conf) + #else: + ## TODO: Fail more verbose + #print "COULD NOT FIND AUTHENTICATION MECHANISM FOR DOMAIN %s" %(domain) + + #print self._auth + + def list_domains(self): + """ + List the domains using the auth_mechanism setting in the kolab + section of the configuration file, either ldap or sql or (...). + + The actual setting would be used by self.connect(), and stuffed + into self._auth, for use with self._auth._list_domains() + + For each domain found, returns a two-part tuple of the primary + domain and a list of secondary domains (aliases). + """ + + # Connect to the global namespace + self.connect() + + # Find the domains in the authentication backend. + kolab_primary_domain = self.conf.get('kolab', 'primary_domain') + domains = self._auth[kolab_primary_domain]._list_domains() + + # If no domains are found, the primary domain is used. + if len(domains) < 1: + domains = [(kolab_primary_domain, [])] + + return domains + + def list_users(self, primary_domain, secondary_domains=[]): + self.connect(domain=primary_domain) + users = self._auth[primary_domain]._list_users(primary_domain, secondary_domains) + #print "USERS RETURNED FROM self._auth['%s']._list_users():", users return users - def set_user_attribute(self, user, attribute, value): - self._connect() - self._auth._set_user_attribute(user, attribute, value) + def domain_default_quota(self, domain): + self.connect(domain=domain) + print self._auth + return self._auth[domain]._domain_default_quota(domain) + + def get_user_attribute(self, user, attribute): + return self._auth[domain]._get_user_attribute(user, attribute) + def set_user_attribute(self, domain, user, attribute, value): + self._auth[domain]._set_user_attribute(user, attribute, value) diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py index ee635d1..8c23cf2 100644 --- a/pykolab/auth/ldap/__init__.py +++ b/pykolab/auth/ldap/__init__.py @@ -16,116 +16,396 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # +import _ldap import ldap +import ldap.async +import ldap.controls +import ldap.resiter +import logging import time +from pykolab import utils from pykolab.conf import Conf from pykolab.constants import * from pykolab.translate import _ class LDAP(object): - def __init__(self, conf=None): - if not conf: - self.conf = Conf() - self.conf.finalize_conf() - self.log = self.conf.log - else: - self.conf = conf - self.log = conf.log + """ + Abstraction layer for the LDAP authentication / authorization backend, + for use with Kolab. + """ + def __init__(self, conf): + self.conf = conf + self.log = logging.getLogger('pykolab.ldap') self.ldap = None + self.bind = False - def _connect(self): + def _authenticate(self, login, domain): + print "Authenticating:", login, domain + self._connect() + user_dn = self._find_dn(login[0], domain) + try: + print "Binding with user_dn %s and password %s" %(user_dn, login[1]) + # Needs to be synchronous or succeeds and continues setting retval to True!! + self.ldap.simple_bind_s(user_dn, login[1]) + retval = True + except: + retval = False + return retval + def _connect(self, domain=None): if not self.ldap == None: return - self.log.debug(_("Connecting to LDAP..."), level=9) - uri = self.conf.get('ldap', 'uri') - self.ldap = ldap.initialize(uri) + if domain == None: + section = 'ldap' + elif not self.conf.has_option(domain, uri): + section = 'ldap' + + self.log.debug(_("Connecting to LDAP...")) + + uri = self.conf.get(section, 'uri') + + self.log.debug(_("Attempting to use LDAP URI %s") %(uri)) + self.ldap = ldap.initialize(uri, trace_level=0) + self.ldap.protocol_version = 3 + + def _bind(self): + # TODO: Implement some mechanism for r/o, r/w and mgmt binding. + self._connect() + + if not self.bind: + # TODO: Use bind credentials for the domain itself. + bind_dn = self.conf.get('ldap', 'bind_dn') + bind_pw = self.conf.get('ldap', 'bind_pw') + # TODO: Binding errors control + try: + self.ldap.simple_bind_s(bind_dn, bind_pw) + except ldap.SERVER_DOWN: + # Can't contact LDAP server + # + # - Service not started + # - Service faulty + # - Firewall + pass + + self.bind = True + + def _unbind(self): + self.ldap.unbind() + self.bind = False + + def _reconnect(self): + self._disconnect() + self._connect() def _disconnect(self): + self._unbind() del self.ldap self.ldap = None + self.bind = False - def _set_user_attribute(self, dn, attribute, value): + def _find_dn(self, login, domain=None): self._connect() - bind_dn = self.conf.get('ldap', 'bind_dn') - bind_pw = self.conf.get('ldap', 'bind_pw') - user_base_dn = self.conf.get('ldap', 'user_base_dn') - kolab_user_filter = self.conf.get('ldap', 'kolab_user_filter') + self._bind() - self.ldap.simple_bind(bind_dn, bind_pw) + if domain == None: + domain = self.conf.get('kolab', 'primary_domain') + + domain_root_dn = self._kolab_domain_root_dn(domain) + + if self.conf.has_option(domain_root_dn, 'user_base_dn'): + section = domain_root_dn + else: + section = 'ldap' + + user_base_dn = self.conf.get_raw(section, 'user_base_dn') %({'base_dn': domain_root_dn}) + + print "user_base_dn:", user_base_dn + + if self.conf.has_option(domain_root_dn, 'kolab_user_filter'): + section = domain_root_dn + else: + section = 'ldap' + + kolab_user_filter = self.conf.get(section, 'kolab_user_filter', quiet=True) + + if kolab_user_filter == "": + kolab_user_filter = self.conf.get('ldap', 'kolab_user_filter') + + search_filter = "(&(%s=%s)%s)" %(self.conf.get('cyrus-sasl', 'result_attribute'),login,kolab_user_filter) + + print search_filter + + _results = self._search(user_base_dn, filterstr=search_filter, attrlist=['dn']) + + if not len(_results) == 1: + return False + + (_user_dn, _user_attrs) = _results[0] + + return _user_dn + + def _search(self, base_dn, scope=ldap.SCOPE_SUBTREE, filterstr="(objectClass=*)", attrlist=None, attrsonly=0, timeout=-1): + _results = [] + + page_size = 500 + critical = True + + server_page_control = ldap.controls.SimplePagedResultsControl( + ldap.LDAP_CONTROL_PAGE_OID, + critical, + (page_size, '') + ) + + _search = self.ldap.search_ext( + base_dn, + scope=scope, + filterstr=filterstr, + attrlist=attrlist, + attrsonly=attrsonly, + serverctrls=[server_page_control] + ) + + pages = 0 + while True: + pages += 1 + #print "Getting page %d..." %(pages) + (_result_type, _result_data, _result_msgid, _result_controls) = self.ldap.result3(_search) + _results.extend(_result_data) + if (pages % 2) == 0: + self.log.debug(_("%d results...") %(len(_results))) + + pctrls = [c for c in _result_controls if c.controlType == ldap.LDAP_CONTROL_PAGE_OID] + + if pctrls: + est, cookie = pctrls[0].controlValue + if cookie: + server_page_control.controlValue = (page_size, cookie) + _search = self.ldap.search_ext( + base_dn, + scope=scope, + filterstr=filterstr, + attrlist=attrlist, + attrsonly=attrsonly, + serverctrls=[server_page_control] + ) + else: + # TODO: Error out more verbose + break + else: + # TODO: Error out more verbose + print "Warning: Server ignores RFC 2696 control." + break + + return _results + + def _result(self, msgid=ldap.RES_ANY, all=1, timeout=-1): + return self.ldap.result(msgid, all, timeout) + + def _domain_default_quota(self, domain): + domain_root_dn = self._kolab_domain_root_dn(domain) + + if self.conf.cfg_parser.has_option(domain_root_dn, 'default_quota'): + return self.conf.get(domain_root_dn, 'default_quota', quiet=True) + elif self.conf.cfg_parser.has_option('ldap', 'default_quota'): + return self.conf.get('ldap', 'default_quota', quiet=True) + elif self.conf.cfg_parser.has_option('kolab', 'default_quota'): + return self.conf.get('kolab', 'default_quota', quiet=True) + + def _get_user_attribute(self, user, attribute): + self._bind() + + (user_dn, user_attrs) = self._search(user, ldap.SCOPE_BASE, '(objectclass=*)', [ attribute ])[0] + + user_attrs = utils.normalize(user_attrs) + return user_attrs[attribute] + + def _set_user_attribute(self, user, attribute, value): + self._bind() + + #print "user:", user + + if type(user) == dict: + user_dn = user['dn'] + elif type(user) == str: + user_dn = user try: - self.ldap.modify(dn, [(ldap.MOD_REPLACE, attribute, value)]) + self.ldap.modify(user_dn, [(ldap.MOD_REPLACE, attribute, value)]) except: - if hasattr(self.conf, "log"): - self.conf.log.warning(_("LDAP modification of attribute %s" + \ - " to value %s failed") %(attribute,value)) - else: - # Cannot but print in case someone's interested - print "LDAP modification of attribute %s to value %s" + \ - " failed" %(attribute,value) - self._disconnect() + self.log.warning(_("LDAP modification of attribute %s" + \ + " for %s to value %s failed") %(attribute,user_dn,value)) + + def _list_domains(self): + """ + Find the domains related to this Kolab setup, and return a list of + DNS domain names. + + Returns a list of tuples, each tuple containing the primary domain + name and a list of secondary domain names. + """ + + self.log.info(_("Listing domains...")) - def _kolab_users(self): self._connect() bind_dn = self.conf.get('ldap', 'bind_dn') bind_pw = self.conf.get('ldap', 'bind_pw') - user_base_dn = self.conf.get('ldap', 'user_base_dn') - kolab_user_filter = self.conf.get('ldap', 'kolab_user_filter') + domain_base_dn = self.conf.get('ldap', 'domain_base_dn', quiet=True) + + if domain_base_dn == "": + # No domains are to be found in LDAP, return an empty list. + # Note that the Auth() base itself handles this case. + return [] + + # If we haven't returned already, let's continue searching + kolab_domain_filter = self.conf.get('ldap', 'kolab_domain_filter') + + # TODO: this function should be wrapped for error handling self.ldap.simple_bind(bind_dn, bind_pw) _search = self.ldap.search( - user_base_dn, + domain_base_dn, ldap.SCOPE_SUBTREE, - kolab_user_filter + kolab_domain_filter, + # TODO: Where we use associateddomain is actually configurable + [ 'associateddomain' ] ) - users = [] + domains = [] _result_type = None while not _result_type == ldap.RES_SEARCH_RESULT: - (_result_type, _users) = self.ldap.result(_search, False, 0) - if not _users == None: - for _user in _users: - user_attrs = {} + try: + (_result_type, _domains) = self._result(_search, False, 0) + except AttributeError, e: + if self.ldap == None: + self._bind() + continue + if not _domains == None: + for _domain in _domains: + (domain_dn, _domain_attrs) = _domain + primary_domain = None + secondary_domains = [] - (user_dn, _user_attrs) = _user - _user_attrs['dn'] = user_dn + _domain_attrs = utils.normalize(_domain_attrs) - self.conf.log.debug(_("Found user %s") %(user_dn), level=9) + # TODO: Where we use associateddomain is actually configurable + if type(_domain_attrs['associateddomain']) == list: + primary_domain = _domain_attrs['associateddomain'].pop(0) + secondary_domains = _domain_attrs['associateddomain'] + else: + primary_domain = _domain_attrs['associateddomain'] - for key in _user_attrs.keys(): - if type(_user_attrs[key]) == list: - if len(_user_attrs[key]) == 1: - user_attrs[key.lower()] = ''.join(_user_attrs[key]) - else: - user_attrs[key.lower()] = _user_attrs[key] - else: - # What the heck? - user_attrs[key.lower()] = _user_attrs[key] + domains.append((primary_domain,secondary_domains)) - # Execute plugin hooks that may change the value(s) of the - # user attributes we are going to be using. - mail = self.conf.plugins.exec_hook("set_user_attrs_mail", args=(user_attrs)) - alternative_mail = self.conf.plugins.exec_hook("set_user_attrs_alternative_mail", args=(user_attrs)) + return domains - if not mail == user_attrs['mail']: - self._set_user_attribute(user_attrs['dn'], "mail", mail) + def _kolab_domain_root_dn(self, domain): + self._bind() - if len(alternative_mail) > 0: - # Also make sure the required object class is available. - if not "mailrecipient" in user_attrs['objectclass']: - user_attrs['objectclass'].append('mailrecipient') - self._set_user_attribute(user_attrs['dn'], 'objectclass', user_attrs['objectclass']) + print "Finding domain root dn for domain %s" %(domain) - self._set_user_attribute(user_attrs['dn'], 'mailalternateaddress', alternative_mail) + bind_dn = self.conf.get('ldap', 'bind_dn') + bind_pw = self.conf.get('ldap', 'bind_pw') - users.append(user_attrs) + domain_base_dn = self.conf.get('ldap', 'domain_base_dn', quiet=True) - return users + if not domain_base_dn == "": + # If we haven't returned already, let's continue searching + domain_name_attribute = self.conf.get('ldap', 'domain_name_attribute') + + _results = self._search( + domain_base_dn, + ldap.SCOPE_SUBTREE, + "(%s=%s)" %(domain_name_attribute,domain) + ) + + domains = [] + for _domain in _results: + (domain_dn, _domain_attrs) = _domain + domain_rootdn_attribute = self.conf.get('ldap', 'domain_rootdn_attribute') + _domain_attrs = utils.normalize(_domain_attrs) + if _domain_attrs.has_key(domain_rootdn_attribute): + return _domain_attrs[domain_rootdn_attribute] + + return utils.standard_root_dn(domain) + + def _list_users(self, primary_domain, secondary_domains=[]): + + self.log.info(_("Listing users for domain %s") %(primary_domain)) + + self._bind() + + # With read-only credentials please + bind_dn = self.conf.get('ldap', 'bind_dn') + #bind_dn = self.conf.get('ldap', 'ro_bind_dn') + bind_pw = self.conf.get('ldap', 'bind_pw') + #bind_pw = self.conf.get('ldap', 'ro_bind_pw') + + domain_root_dn = self._kolab_domain_root_dn(primary_domain) + + if self.conf.has_option(domain_root_dn, 'user_base_dn'): + section = domain_root_dn + else: + section = 'ldap' + + user_base_dn = self.conf.get_raw(section, 'user_base_dn') %({'base_dn': domain_root_dn}) + + if self.conf.has_option(domain_root_dn, 'kolab_user_filter'): + section = domain_root_dn + else: + section = 'ldap' + + kolab_user_filter = self.conf.get(section, 'kolab_user_filter', quiet=True) + + if kolab_user_filter == "": + kolab_user_filter = self.conf.get('ldap', 'kolab_user_filter') + + self.ldap.simple_bind(bind_dn, bind_pw) + + # TODO: For (very) large result sets, this may hit a limit and we need + # it to just continue. + # + _search = self._search( + user_base_dn, + ldap.SCOPE_SUBTREE, + kolab_user_filter, + attrlist=[ 'dn', 'mail', 'sn', 'givenname', 'cn', 'uid' ], + attrsonly=0 + ) + + self.log.debug(_("Iterating over %d users, making sure we have the necessary attributes...") %(len(_search))) + + #print "SEARCH RESULTS:", _search + + users = [] + _result_type = None + + for user_dn, user_attrs in _search: + user = {} + user['dn'] = user_dn + if not user.has_key('standard_domain'): + user['standard_domain'] = (primary_domain, secondary_domains) + + user_attrs = utils.normalize(user_attrs) + + #print "USER_ATTRS:", user_attrs + + for attribute in [ 'mail', 'sn', 'givenname', 'cn', 'uid' ]: + if not user_attrs.has_key(attribute): + #print "doesn't have attribute" + user[attribute] = self._get_user_attribute(user_dn, attribute) + else: + #print "has attribute" + user[attribute] = user_attrs[attribute] + + users.append(user) + + #print "USERS:", users + + return users |