Back to index

moin  1.9.0~rc2
ldap_login.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - LDAP / Active Directory authentication
00004 
00005     This code only creates a user object, the session will be established by
00006     moin automatically.
00007 
00008     python-ldap needs to be at least 2.0.0pre06 (available since mid 2002) for
00009     ldaps support - some older debian installations (woody and older?) require
00010     libldap2-tls and python2.x-ldap-tls, otherwise you get ldap.SERVER_DOWN:
00011     "Can't contact LDAP server" - more recent debian installations have tls
00012     support in libldap2 (see dependency on gnutls) and also in python-ldap.
00013 
00014     TODO: allow more configuration (alias name, ...) by using callables as parameters
00015 
00016     @copyright: 2006-2008 MoinMoin:ThomasWaldmann,
00017                 2006 Nick Phillips
00018     @license: GNU GPL, see COPYING for details.
00019 """
00020 from MoinMoin import log
00021 logging = log.getLogger(__name__)
00022 
00023 try:
00024     import ldap
00025 except ImportError, err:
00026     logging.error("You need to have python-ldap installed (%s)." % str(err))
00027     raise
00028 
00029 from MoinMoin import user
00030 from MoinMoin.auth import BaseAuth, CancelLogin, ContinueLogin
00031 
00032 
00033 class LDAPAuth(BaseAuth):
00034     """ get authentication data from form, authenticate against LDAP (or Active
00035         Directory), fetch some user infos from LDAP and create a user object
00036         for that user. The session is kept by moin automatically.
00037     """
00038 
00039     login_inputs = ['username', 'password']
00040     logout_possible = True
00041     name = 'ldap'
00042 
00043     def __init__(self,
00044         server_uri='ldap://localhost',  # ldap / active directory server URI
00045                                         # use ldaps://server:636 url for ldaps,
00046                                         # use  ldap://server for ldap without tls (and set start_tls to 0),
00047                                         # use  ldap://server for ldap with tls (and set start_tls to 1 or 2).
00048         bind_dn='',  # We can either use some fixed user and password for binding to LDAP.
00049                      # Be careful if you need a % char in those strings - as they are used as
00050                      # a format string, you have to write %% to get a single % in the end.
00051                      #bind_dn = 'binduser@example.org' # (AD)
00052                      #bind_dn = 'cn=admin,dc=example,dc=org' # (OpenLDAP)
00053                      #bind_pw = 'secret'
00054                      # or we can use the username and password we got from the user:
00055                      #bind_dn = '%(username)s@example.org' # DN we use for first bind (AD)
00056                      #bind_pw = '%(password)s' # password we use for first bind
00057                      # or we can bind anonymously (if that is supported by your directory).
00058                      # In any case, bind_dn and bind_pw must be defined.
00059         bind_pw='',
00060         base_dn='',  # base DN we use for searching
00061                      #base_dn = 'ou=SOMEUNIT,dc=example,dc=org'
00062         scope=ldap.SCOPE_SUBTREE, # scope of the search we do (2 == ldap.SCOPE_SUBTREE)
00063         referrals=0, # LDAP REFERRALS (0 needed for AD)
00064         search_filter='(uid=%(username)s)',  # ldap filter used for searching:
00065                                              #search_filter = '(sAMAccountName=%(username)s)' # (AD)
00066                                              #search_filter = '(uid=%(username)s)' # (OpenLDAP)
00067                                              # you can also do more complex filtering like:
00068                                              # "(&(cn=%(username)s)(memberOf=CN=WikiUsers,OU=Groups,DC=example,DC=org))"
00069         # some attribute names we use to extract information from LDAP:
00070         givenname_attribute=None, # ('givenName') ldap attribute we get the first name from
00071         surname_attribute=None, # ('sn') ldap attribute we get the family name from
00072         aliasname_attribute=None, # ('displayName') ldap attribute we get the aliasname from
00073         email_attribute=None, # ('mail') ldap attribute we get the email address from
00074         email_callback=None, # called to make up email address
00075         name_callback=None, # called to use a Wiki name different from the login name
00076         coding='utf-8', # coding used for ldap queries and result values
00077         timeout=10, # how long we wait for the ldap server [s]
00078         start_tls=0, # 0 = No, 1 = Try, 2 = Required
00079         tls_cacertdir=None,
00080         tls_cacertfile=None,
00081         tls_certfile=None,
00082         tls_keyfile=None,
00083         tls_require_cert=0, # 0 == ldap.OPT_X_TLS_NEVER (needed for self-signed certs)
00084         bind_once=False, # set to True to only do one bind - useful if configured to bind as the user on the first attempt
00085         autocreate=False, # set to True if you want to autocreate user profiles
00086         name='ldap', # use e.g. 'ldap_pdc' and 'ldap_bdc' (or 'ldap1' and 'ldap2') if you auth against 2 ldap servers
00087         ):
00088         self.server_uri = server_uri
00089         self.bind_dn = bind_dn
00090         self.bind_pw = bind_pw
00091         self.base_dn = base_dn
00092         self.scope = scope
00093         self.referrals = referrals
00094         self.search_filter = search_filter
00095 
00096         self.givenname_attribute = givenname_attribute
00097         self.surname_attribute = surname_attribute
00098         self.aliasname_attribute = aliasname_attribute
00099         self.email_attribute = email_attribute
00100         self.email_callback = email_callback
00101         self.name_callback = name_callback
00102 
00103         self.coding = coding
00104         self.timeout = timeout
00105 
00106         self.start_tls = start_tls
00107         self.tls_cacertdir = tls_cacertdir
00108         self.tls_cacertfile = tls_cacertfile
00109         self.tls_certfile = tls_certfile
00110         self.tls_keyfile = tls_keyfile
00111         self.tls_require_cert = tls_require_cert
00112 
00113         self.bind_once = bind_once
00114         self.autocreate = autocreate
00115         self.name = name
00116 
00117     def login(self, request, user_obj, **kw):
00118         username = kw.get('username')
00119         password = kw.get('password')
00120         _ = request.getText
00121 
00122 
00123         # we require non-empty password as ldap bind does a anon (not password
00124         # protected) bind if the password is empty and SUCCEEDS!
00125         if not password:
00126             return ContinueLogin(user_obj, _('Missing password. Please enter user name and password.'))
00127 
00128         try:
00129             try:
00130                 u = None
00131                 dn = None
00132                 coding = self.coding
00133                 logging.debug("Setting misc. ldap options...")
00134                 ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
00135                 ldap.set_option(ldap.OPT_REFERRALS, self.referrals)
00136                 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
00137 
00138                 if hasattr(ldap, 'TLS_AVAIL') and ldap.TLS_AVAIL:
00139                     for option, value in (
00140                         (ldap.OPT_X_TLS_CACERTDIR, self.tls_cacertdir),
00141                         (ldap.OPT_X_TLS_CACERTFILE, self.tls_cacertfile),
00142                         (ldap.OPT_X_TLS_CERTFILE, self.tls_certfile),
00143                         (ldap.OPT_X_TLS_KEYFILE, self.tls_keyfile),
00144                         (ldap.OPT_X_TLS_REQUIRE_CERT, self.tls_require_cert),
00145                         (ldap.OPT_X_TLS, self.start_tls),
00146                         #(ldap.OPT_X_TLS_ALLOW, 1),
00147                     ):
00148                         if value is not None:
00149                             ldap.set_option(option, value)
00150 
00151                 server = self.server_uri
00152                 logging.debug("Trying to initialize %r." % server)
00153                 l = ldap.initialize(server)
00154                 logging.debug("Connected to LDAP server %r." % server)
00155 
00156                 if self.start_tls and server.startswith('ldap:'):
00157                     logging.debug("Trying to start TLS to %r." % server)
00158                     try:
00159                         l.start_tls_s()
00160                         logging.debug("Using TLS to %r." % server)
00161                     except (ldap.SERVER_DOWN, ldap.CONNECT_ERROR), err:
00162                         logging.warning("Couldn't establish TLS to %r (err: %s)." % (server, str(err)))
00163                         raise
00164 
00165                 # you can use %(username)s and %(password)s here to get the stuff entered in the form:
00166                 binddn = self.bind_dn % locals()
00167                 bindpw = self.bind_pw % locals()
00168                 l.simple_bind_s(binddn.encode(coding), bindpw.encode(coding))
00169                 logging.debug("Bound with binddn %r" % binddn)
00170 
00171                 # you can use %(username)s here to get the stuff entered in the form:
00172                 filterstr = self.search_filter % locals()
00173                 logging.debug("Searching %r" % filterstr)
00174                 attrs = [getattr(self, attr) for attr in [
00175                                          'email_attribute',
00176                                          'aliasname_attribute',
00177                                          'surname_attribute',
00178                                          'givenname_attribute',
00179                                          ] if getattr(self, attr) is not None]
00180                 lusers = l.search_st(self.base_dn, self.scope, filterstr.encode(coding),
00181                                      attrlist=attrs, timeout=self.timeout)
00182                 # we remove entries with dn == None to get the real result list:
00183                 lusers = [(dn, ldap_dict) for dn, ldap_dict in lusers if dn is not None]
00184                 for dn, ldap_dict in lusers:
00185                     logging.debug("dn:%r" % dn)
00186                     for key, val in ldap_dict.items():
00187                         logging.debug("    %r: %r" % (key, val))
00188 
00189                 result_length = len(lusers)
00190                 if result_length != 1:
00191                     if result_length > 1:
00192                         logging.warning("Search found more than one (%d) matches for %r." % (result_length, filterstr))
00193                     if result_length == 0:
00194                         logging.debug("Search found no matches for %r." % (filterstr, ))
00195                     return ContinueLogin(user_obj, _("Invalid username or password."))
00196 
00197                 dn, ldap_dict = lusers[0]
00198                 if not self.bind_once:
00199                     logging.debug("DN found is %r, trying to bind with pw" % dn)
00200                     l.simple_bind_s(dn, password.encode(coding))
00201                     logging.debug("Bound with dn %r (username: %r)" % (dn, username))
00202 
00203                 if self.email_callback is None:
00204                     if self.email_attribute:
00205                         email = ldap_dict.get(self.email_attribute, [''])[0].decode(coding)
00206                     else:
00207                         email = None
00208                 else:
00209                     email = self.email_callback(ldap_dict)
00210 
00211                 aliasname = ''
00212                 try:
00213                     aliasname = ldap_dict[self.aliasname_attribute][0]
00214                 except (KeyError, IndexError):
00215                     pass
00216                 if not aliasname:
00217                     sn = ldap_dict.get(self.surname_attribute, [''])[0]
00218                     gn = ldap_dict.get(self.givenname_attribute, [''])[0]
00219                     if sn and gn:
00220                         aliasname = "%s, %s" % (sn, gn)
00221                     elif sn:
00222                         aliasname = sn
00223                 aliasname = aliasname.decode(coding)
00224 
00225                 if self.name_callback:
00226                     username = self.name_callback(ldap_dict)
00227 
00228                 if email:
00229                     u = user.User(request, auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'email', 'mailto_author', ))
00230                     u.email = email
00231                 else:
00232                     u = user.User(request, auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'mailto_author', ))
00233                 u.name = username
00234                 u.aliasname = aliasname
00235                 u.remember_me = 0 # 0 enforces cookie_lifetime config param
00236                 logging.debug("creating user object with name %r email %r alias %r" % (username, email, aliasname))
00237 
00238             except ldap.INVALID_CREDENTIALS, err:
00239                 logging.debug("invalid credentials (wrong password?) for dn %r (username: %r)" % (dn, username))
00240                 return CancelLogin(_("Invalid username or password."))
00241 
00242             if u and self.autocreate:
00243                 logging.debug("calling create_or_update to autocreate user %r" % u.name)
00244                 u.create_or_update(True)
00245             return ContinueLogin(u)
00246 
00247         except ldap.SERVER_DOWN, err:
00248             # looks like this LDAP server isn't working, so we just try the next
00249             # authenticator object in cfg.auth list (there could be some second
00250             # ldap authenticator that queries a backup server or any other auth
00251             # method).
00252             logging.error("LDAP server %s failed (%s). "
00253                           "Trying to authenticate with next auth list entry." % (server, str(err)))
00254             return ContinueLogin(user_obj, _("LDAP server %(server)s failed.") % {'server': server})
00255 
00256         except:
00257             logging.exception("caught an exception, traceback follows...")
00258             return ContinueLogin(user_obj)
00259