summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com>2011-03-07 15:09:51 +0000
committerJeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com>2011-03-07 15:09:51 +0000
commita593f9575820e6c029a8ff37d7df6253f35d0e84 (patch)
tree9d444534853d15ebc44f8bfa25d614f463bacef1
parenteb8353dd68d593dffb8a8b40d3b16ab665635892 (diff)
downloadpykolab-a593f9575820e6c029a8ff37d7df6253f35d0e84.tar.gz
Resort module imports
Just use the Python loggin library logger Standardize function names a little better Make LDAP page its results so large sets of search results hit no admin or search limit
-rw-r--r--pykolab/auth/__init__.py128
-rw-r--r--pykolab/auth/ldap/__init__.py406
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