From 829f13caed40bb7702d3653d76dc56a3c2bded19 Mon Sep 17 00:00:00 2001
From: Martin Wilck <martin.wilck@ts.fujitsu.com>
Date: Fri, 25 May 2012 16:06:38 +0200
Subject: [PATCH 2/2] PATCH: [LDAPAuth] implement SASL bind

This patch implements SASL binding for LDAP auth. This has several
advantages over simple LDAP binding: No bind DN is necessary, and
advanced authentication mechanisms are avaiable (e.g. avoiding
plain-text transmission of passwords).

DIGEST-MD5 tested (that's the only method my LDAP server supports),
but other methods should also work.
---
 auth/ldap_login.py |   40 ++++++++++++++++++++++++++++++++++++++--
 1 files changed, 38 insertions(+), 2 deletions(-)

diff --git a/auth/ldap_login.py b/auth/ldap_login.py
index ba93098..5482e8a 100644
--- a/auth/ldap_login.py
+++ b/auth/ldap_login.py
@@ -26,6 +26,15 @@ except ImportError, err:
     logging.error("You need to have python-ldap installed (%s)." % str(err))
     raise
 
+try:
+    import ldap.sasl as ldap_sasl
+except ImportError, err:
+    logging.error("SASL module not found, SASL bind will not be possible (%s)." % str(err))
+    ldap.sasl = None
+else:
+    ldap.sasl = ldap_sasl
+    del ldap_sasl
+
 from MoinMoin import user
 from MoinMoin.auth import BaseAuth, ContinueLogin
 
@@ -85,6 +94,7 @@ class LDAPAuth(BaseAuth):
         autocreate=False, # set to True if you want to autocreate user profiles
         name='ldap', # use e.g. 'ldap_pdc' and 'ldap_bdc' (or 'ldap1' and 'ldap2') if you auth against 2 ldap servers
         report_invalid_credentials=True, # whether to emit "invalid username or password" msg at login time or not
+        sasl_mech=None, # The SASL mechnism to use - this activates SASL
         ):
         self.server_uri = server_uri
         self.bind_dn = bind_dn
@@ -117,6 +127,24 @@ class LDAPAuth(BaseAuth):
 
         self.report_invalid_credentials = report_invalid_credentials
 
+        if ldap.sasl is None:
+            self.sasl_mech = None
+        else:
+            self.sasl_mech = sasl_mech.upper()
+            self.bind_once = True # There's only 1 bind with SASL
+
+    def sasl_bind(self, connection, authname, passwd):
+        if self.sasl_mech in ("GSSAPI", "EXTERNAL"):
+            authargs = {}
+        else:
+            authargs = {
+                ldap.sasl.CB_AUTHNAME: authname.encode(self.coding),
+                ldap.sasl.CB_PASS: passwd.encode(self.coding)
+                }
+        logging.debug("Attempting SASL/%s bind for %s" % (self.sasl_mech, authname))
+        authtok = ldap.sasl.sasl(authargs, self.sasl_mech)
+        connection.sasl_interactive_bind_s("", authtok)
+
     def login(self, request, user_obj, **kw):
         username = kw.get('username')
         password = kw.get('password')
@@ -168,8 +196,11 @@ class LDAPAuth(BaseAuth):
                 # you can use %(username)s and %(password)s here to get the stuff entered in the form:
                 binddn = self.bind_dn % locals()
                 bindpw = self.bind_pw % locals()
-                l.simple_bind_s(binddn.encode(coding), bindpw.encode(coding))
-                logging.debug("Bound with binddn %r" % binddn)
+                if self.sasl_mech is None:
+                    l.simple_bind_s(binddn.encode(coding), bindpw.encode(coding))
+                    logging.debug("Bound with binddn %r" % binddn)
+                else:
+                    self.sasl_bind(l, binddn, bindpw)
 
                 # you can use %(username)s here to get the stuff entered in the form:
                 filterstr = self.search_filter % locals()
@@ -265,6 +296,11 @@ class LDAPAuth(BaseAuth):
                           "Trying to authenticate with next auth list entry." % (server, str(err)))
             return ContinueLogin(user_obj, _("LDAP server %(server)s failed.") % {'server': server})
 
+        except (ldap.STRONG_AUTH_NOT_SUPPORTED, ldap.STRONG_AUTH_REQUIRED, ldap.AUTH_UNKNOWN), err:
+            logging.error("Authentication to LDAP server %s failed (%s)."
+                          "Trying to authenticate with next auth list entry." % (server, str(err)))
+            return ContinueLogin(user_obj, _("LDAP server %(server)s failed.") % {'server': server})
+            
         except:
             logging.exception("caught an exception, traceback follows...")
             return ContinueLogin(user_obj)
-- 
1.7.7.6

