changeset 991:b232a36cc51f issue-108

Improve LDAP authentication * Adds an LDAP filter for locating the LDAP object * Adds a search scope policy when using the Base DN * Adds option required certificate policy when using LDAPS * Adds attribute mapping for username, firstname, lastname, email * Initializes rhodecode user using LDAP info (no longer uses "@ldap") * Remembers the user object (DN) in the user table * Updates admin interfaces * Authenticates against actual user objects in LDAP * Possibly other things. Really, this should be extended to a list of LDAP configurations, but this is a good start.
author Thayne Harbaugh <thayne@fusionio.com>
date Thu, 03 Feb 2011 16:34:40 -0700
parents 7a1df0130533
children c03d16787b5c
files rhodecode/controllers/admin/ldap_settings.py rhodecode/lib/auth.py rhodecode/lib/auth_ldap.py rhodecode/lib/db_manage.py rhodecode/model/db.py rhodecode/model/forms.py rhodecode/model/settings.py rhodecode/model/user.py rhodecode/templates/admin/ldap/ldap.html rhodecode/templates/admin/users/user_edit.html rhodecode/templates/admin/users/users.html
diffstat 11 files changed, 154 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- a/rhodecode/controllers/admin/ldap_settings.py	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/controllers/admin/ldap_settings.py	Thu Feb 03 16:34:40 2011 -0700
@@ -48,15 +48,33 @@
 
 class LdapSettingsController(BaseController):
 
+    search_scope_choices = [('BASE',     _('BASE'),),
+                            ('ONELEVEL', _('ONELEVEL'),),
+                            ('SUBTREE',  _('SUBTREE'),),
+                            ]
+    search_scope_default = 'SUBTREE'
+
+    tls_reqcert_choices = [('NEVER',  _('NEVER'),),
+                           ('ALLOW',  _('ALLOW'),),
+                           ('TRY',    _('TRY'),),
+                           ('DEMAND', _('DEMAND'),),
+                           ('HARD',   _('HARD'),),
+                           ]
+    tls_reqcert_default = 'DEMAND'
+
     @LoginRequired()
     @HasPermissionAllDecorator('hg.admin')
     def __before__(self):
         c.admin_user = session.get('admin_user')
         c.admin_username = session.get('admin_username')
+        c.search_scope_choices = self.search_scope_choices
+        c.tls_reqcert_choices  = self.tls_reqcert_choices
         super(LdapSettingsController, self).__before__()
 
     def index(self):
         defaults = SettingsModel().get_ldap_settings()
+        c.search_scope_cur = defaults.get('ldap_search_scope')
+        c.tls_reqcert_cur  = defaults.get('ldap_tls_reqcert')
 
         return htmlfill.render(
                     render('admin/ldap/ldap.html'),
@@ -68,7 +86,8 @@
         """POST ldap create and store ldap settings"""
 
         settings_model = SettingsModel()
-        _form = LdapSettingsForm()()
+        _form = LdapSettingsForm([x[0] for x in self.tls_reqcert_choices],
+                                 [x[0] for x in self.search_scope_choices])()
 
         try:
             form_result = _form.to_python(dict(request.POST))
@@ -91,6 +110,9 @@
 
         except formencode.Invalid, errors:
 
+            c.search_scope_cur = self.search_scope_default
+            c.tls_reqcert_cur  = self.search_scope_default
+
             return htmlfill.render(
                 render('admin/ldap/ldap.html'),
                 defaults=errors.value,
--- a/rhodecode/lib/auth.py	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/lib/auth.py	Thu Feb 03 16:34:40 2011 -0700
@@ -103,7 +103,7 @@
     user = user_model.get_by_username(username, cache=False)
 
     log.debug('Authenticating user using RhodeCode account')
-    if user is not None and user.is_ldap is False:
+    if user is not None and not user.ldap_dn:
         if user.active:
 
             if user.username == 'default' and user.active:
@@ -122,7 +122,7 @@
         user_obj = user_model.get_by_username(username, cache=False,
                                             case_insensitive=True)
 
-        if user_obj is not None and user_obj.is_ldap is False:
+        if user_obj is not None and not user_obj.ldap_dn:
             log.debug('this user already exists as non ldap')
             return False
 
@@ -141,15 +141,25 @@
                   'bind_dn':ldap_settings.get('ldap_dn_user'),
                   'bind_pass':ldap_settings.get('ldap_dn_pass'),
                   'use_ldaps':ldap_settings.get('ldap_ldaps'),
+                  'tls_reqcert':ldap_settings.get('ldap_tls_reqcert'),
+                  'ldap_filter':ldap_settings.get('ldap_filter'),
+                  'search_scope':ldap_settings.get('ldap_search_scope'),
+                  'attr_login':ldap_settings.get('ldap_attr_login'),
                   'ldap_version':3,
                   }
             log.debug('Checking for ldap authentication')
             try:
                 aldap = AuthLdap(**kwargs)
-                res = aldap.authenticate_ldap(username, password)
-                log.debug('Got ldap response %s', res)
+                (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
+                log.debug('Got ldap DN response %s', user_dn)
 
-                if user_model.create_ldap(username, password):
+                user_attrs = {
+                    'name'     : ldap_attrs[ldap_settings.get('ldap_attr_firstname')][0],
+                    'lastname' : ldap_attrs[ldap_settings.get('ldap_attr_lastname')][0],
+                    'email'    : ldap_attrs[ldap_settings.get('ldap_attr_email')][0],
+                    }
+
+                if user_model.create_ldap(username, password, user_dn, user_attrs):
                     log.info('created new ldap user')
 
                 return True
--- a/rhodecode/lib/auth_ldap.py	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/lib/auth_ldap.py	Thu Feb 03 16:34:40 2011 -0700
@@ -36,11 +36,15 @@
 class AuthLdap(object):
 
     def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
-                 use_ldaps=False, ldap_version=3):
+                 use_ldaps=False, tls_reqcert='DEMAND', ldap_version=3,
+                 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))',
+                 search_scope='SUBTREE',
+                 attr_login='uid'):
         self.ldap_version = ldap_version
         if use_ldaps:
             port = port or 689
         self.LDAP_USE_LDAPS = use_ldaps
+        self.TLS_REQCERT = ldap.__dict__['OPT_X_TLS_' + tls_reqcert]
         self.LDAP_SERVER_ADDRESS = server
         self.LDAP_SERVER_PORT = port
 
@@ -55,6 +59,10 @@
                                                self.LDAP_SERVER_PORT)
 
         self.BASE_DN = base_dn
+        self.LDAP_FILTER = ldap_filter
+        self.SEARCH_SCOPE = ldap.__dict__['SCOPE_' + search_scope]
+        self.attr_login = attr_login
+
 
     def authenticate_ldap(self, username, password):
         """Authenticate a user via LDAP and return his/her LDAP properties.
@@ -74,7 +82,13 @@
             raise LdapUsernameError("invalid character in username: ,")
         try:
             ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, '/etc/openldap/cacerts')
+            ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
+            ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
+            ldap.set_option(ldap.OPT_TIMEOUT, 20)
             ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
+            ldap.set_option(ldap.OPT_TIMELIMIT, 15)
+            if self.LDAP_USE_LDAPS:
+                ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
             server = ldap.initialize(self.LDAP_SERVER)
             if self.ldap_version == 2:
                 server.protocol = ldap.VERSION2
@@ -84,21 +98,29 @@
             if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
                 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
 
-            dn = self.BASE_DN % {'user':uid}
-            log.debug("Authenticating %r at %s", dn, self.LDAP_SERVER)
-            server.simple_bind_s(dn, password)
+            filt = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login, username)
+            log.debug("Authenticating %r filt %s at %s", self.BASE_DN, filt, self.LDAP_SERVER)
+            lobjects = server.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE, filt)
+
+            if not lobjects:
+                raise ldap.NO_SUCH_OBJECT()
 
-            properties = server.search_s(dn, ldap.SCOPE_SUBTREE)
-            if not properties:
-                raise ldap.NO_SUCH_OBJECT()
+            for (dn, attrs) in lobjects:
+                try:
+                    server.simple_bind_s(dn, password)
+                    break
+
+                except ldap.INVALID_CREDENTIALS, e:
+                    log.debug("LDAP rejected password for user '%s' (%s): %s", uid, username, dn)
+
+                else:
+                    log.debug("No matching LDAP objecs for authentication of '%s' (%s)", uid, username)
+                    raise LdapPasswordError()
+
         except ldap.NO_SUCH_OBJECT, e:
             log.debug("LDAP says no such user '%s' (%s)", uid, username)
             raise LdapUsernameError()
-        except ldap.INVALID_CREDENTIALS, e:
-            log.debug("LDAP rejected password for user '%s' (%s)", uid, username)
-            raise LdapPasswordError()
         except ldap.SERVER_DOWN, e:
             raise LdapConnectionError("LDAP can't access authentication server")
 
-        return properties[0]
-
+        return (dn, attrs)
--- a/rhodecode/lib/db_manage.py	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/lib/db_manage.py	Thu Feb 03 16:34:40 2011 -0700
@@ -336,7 +336,10 @@
 
         try:
             for k in ['ldap_active', 'ldap_host', 'ldap_port', 'ldap_ldaps',
-                      'ldap_dn_user', 'ldap_dn_pass', 'ldap_base_dn']:
+                      'ldap_tls_reqcert', 'ldap_dn_user', 'ldap_dn_pass',
+                      'ldap_base_dn', 'ldap_filter', 'ldap_search_scope',
+                      'ldap_attr_login', 'ldap_attr_firstname', 'ldap_attr_lastname',
+                      'ldap_attr_email']:
 
                 setting = RhodeCodeSettings(k, '')
                 self.sa.add(setting)
--- a/rhodecode/model/db.py	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/model/db.py	Thu Feb 03 16:34:40 2011 -0700
@@ -105,7 +105,7 @@
     lastname = Column("lastname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     email = Column("email", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
-    is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
+    ldap_dn = Column("ldap_dn", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
 
     user_log = relationship('UserLog', cascade='all')
     user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
--- a/rhodecode/model/forms.py	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/model/forms.py	Thu Feb 03 16:34:40 2011 -0700
@@ -334,23 +334,14 @@
             raise LdapImportError
         return value
 
-class BaseDnValidator(formencode.validators.FancyValidator):
+class AttrLoginValidator(formencode.validators.FancyValidator):
 
     def to_python(self, value, state):
 
-        try:
-            value % {'user':'valid'}
-
-            if value.find('%(user)s') == -1:
-                raise formencode.Invalid(_("You need to specify %(user)s in "
-                                           "template for example uid=%(user)s "
-                                           ",dc=company...") ,
-                                         value, state)
-
-        except KeyError:
-            raise formencode.Invalid(_("Wrong template used, only %(user)s "
-                                       "is an valid entry") ,
-                                         value, state)
+        if not value or not isinstance(value, (str, unicode)):
+            raise formencode.Invalid(_("The LDAP Login attribute of the CN must be specified "
+                                       "- this is the name of the attribute that is equivalent to 'username'"),
+                                     value, state)
 
         return value
 
@@ -521,7 +512,7 @@
     return _DefaultPermissionsForm
 
 
-def LdapSettingsForm():
+def LdapSettingsForm(tls_reqcert_choices, search_scope_choices):
     class _LdapSettingsForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
@@ -530,8 +521,15 @@
         ldap_host = UnicodeString(strip=True,)
         ldap_port = Number(strip=True,)
         ldap_ldaps = StringBoolean(if_missing=False)
+        ldap_tls_reqcert = OneOf(tls_reqcert_choices)
         ldap_dn_user = UnicodeString(strip=True,)
         ldap_dn_pass = UnicodeString(strip=True,)
-        ldap_base_dn = All(BaseDnValidator, UnicodeString(strip=True,))
+        ldap_base_dn = UnicodeString(strip=True,)
+        ldap_filter = UnicodeString(strip=True,)
+        ldap_search_scope = OneOf(search_scope_choices)
+        ldap_attr_login = All(AttrLoginValidator, UnicodeString(strip=True,))
+        ldap_attr_firstname = UnicodeString(strip=True,)
+        ldap_attr_lastname = UnicodeString(strip=True,)
+        ldap_attr_email = UnicodeString(strip=True,)
 
     return _LdapSettingsForm
--- a/rhodecode/model/settings.py	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/model/settings.py	Thu Feb 03 16:34:40 2011 -0700
@@ -72,10 +72,18 @@
         ldap_host
         ldap_port 
         ldap_ldaps
+        ldap_tls_reqcert
         ldap_dn_user 
         ldap_dn_pass 
         ldap_base_dn
+        ldap_filter
+        ldap_search_scope
+        ldap_attr_login
+        ldap_attr_firstname
+        ldap_attr_lastname
+        ldap_attr_email
         """
+        # ldap_search_scope
 
         r = self.sa.query(RhodeCodeSettings)\
                 .filter(RhodeCodeSettings.app_settings_name\
--- a/rhodecode/model/user.py	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/model/user.py	Thu Feb 03 16:34:40 2011 -0700
@@ -75,25 +75,27 @@
             self.sa.rollback()
             raise
 
-    def create_ldap(self, username, password):
+    def create_ldap(self, username, password, user_dn, attrs):
         """
         Checks if user is in database, if not creates this user marked
         as ldap user
         :param username:
         :param password:
+        :param user_dn:
+        :param attrs:
         """
         from rhodecode.lib.auth import get_crypt_password
         log.debug('Checking for such ldap account in RhodeCode database')
         if self.get_by_username(username, case_insensitive=True) is None:
             try:
                 new_user = User()
-                new_user.username = username.lower()#add ldap account always lowercase
+                new_user.username = username.lower() # add ldap account always lowercase
                 new_user.password = get_crypt_password(password)
-                new_user.email = '%s@ldap.server' % username
+                new_user.email = attrs['email']
                 new_user.active = True
-                new_user.is_ldap = True
-                new_user.name = '%s@ldap' % username
-                new_user.lastname = ''
+                new_user.ldap_dn = user_dn
+                new_user.name = attrs['name']
+                new_user.lastname = attrs['lastname']
 
 
                 self.sa.add(new_user)
--- a/rhodecode/templates/admin/ldap/ldap.html	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/templates/admin/ldap/ldap.html	Thu Feb 03 16:34:40 2011 -0700
@@ -21,13 +21,13 @@
     <div class="title">
         ${self.breadcrumbs()}       
     </div>
-    <h3>${_('LDAP administration')}</h3>
     ${h.form(url('ldap_settings'))}
     <div class="form">
         <div class="fields">
 
+	  <h3>${_('Connection settings')}</h3>
             <div class="field">
-                <div class="label label-checkbox"><label for="ldap_active">${_('Enable ldap')}</label></div>
+                <div class="label label-checkbox"><label for="ldap_active">${_('Enable LDAP')}</label></div>
                 <div class="checkboxes"><div class="checkbox">${h.checkbox('ldap_active',True,class_='small')}</div></div>
             </div>
             <div class="field">
@@ -39,10 +39,6 @@
                 <div class="input">${h.text('ldap_port',class_='small')}</div>
             </div>
             <div class="field">
-                <div class="label label-checkbox"><label for="ldap_ldaps">${_('Enable LDAPS')}</label></div>
-                <div class="checkboxes"><div class="checkbox">${h.checkbox('ldap_ldaps',True,class_='small')}</div></div>
-            </div>
-            <div class="field">
                 <div class="label"><label for="ldap_dn_user">${_('Account')}</label></div>
                 <div class="input">${h.text('ldap_dn_user',class_='small')}</div>
             </div>
@@ -51,9 +47,43 @@
                 <div class="input">${h.password('ldap_dn_pass',class_='small')}</div>
             </div>
             <div class="field">
+                <div class="label label-checkbox"><label for="ldap_ldaps">${_('Enable LDAPS')}</label></div>
+                <div class="checkboxes"><div class="checkbox">${h.checkbox('ldap_ldaps',True,class_='small')}</div></div>
+            </div>
+            <div class="field">
+                <div class="label"><label for="ldap_tls_reqcert">${_('Certificate Checks')}</label></div>
+                <div class="select">${h.select('ldap_tls_reqcert',c.tls_reqcert_cur,c.tls_reqcert_choices,class_='small')}</div>
+            </div>
+	  <h3>${_('Search settings')}</h3>
+            <div class="field">
                 <div class="label"><label for="ldap_base_dn">${_('Base DN')}</label></div>
                 <div class="input">${h.text('ldap_base_dn',class_='small')}</div>
             </div>
+            <div class="field">
+                <div class="label"><label for="ldap_filter">${_('LDAP Filter')}</label></div>
+                <div class="input">${h.text('ldap_filter',class_='small')}</div>
+            </div>
+            <div class="field">
+                <div class="label"><label for="ldap_search_scope">${_('LDAP Search Scope')}</label></div>
+                <div class="select">${h.select('ldap_search_scope',c.search_scope_cur,c.search_scope_choices,class_='small')}</div>
+            </div>
+	  <h3>${_('Attribute mappings')}</h3>
+            <div class="field">
+                <div class="label"><label for="ldap_attr_login">${_('Login Attribute')}</label></div>
+                <div class="input">${h.text('ldap_attr_login',class_='small')}</div>
+            </div>
+            <div class="field">
+                <div class="label"><label for="ldap_attr_firstname">${_('First Name Attribute')}</label></div>
+                <div class="input">${h.text('ldap_attr_firstname',class_='small')}</div>
+            </div>
+            <div class="field">
+                <div class="label"><label for="ldap_attr_lastname">${_('Last Name Attribute')}</label></div>
+                <div class="input">${h.text('ldap_attr_lastname',class_='small')}</div>
+            </div>
+            <div class="field">
+                <div class="label"><label for="ldap_attr_email">${_('E-mail Attribute')}</label></div>
+                <div class="input">${h.text('ldap_attr_email',class_='small')}</div>
+            </div>
             
             <div class="buttons">
             ${h.submit('save','Save',class_="ui-button")}
--- a/rhodecode/templates/admin/users/user_edit.html	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/templates/admin/users/user_edit.html	Thu Feb 03 16:34:40 2011 -0700
@@ -49,6 +49,15 @@
             
              <div class="field">
                 <div class="label">
+                    <label for="ldap_dn">${_('LDAP DN')}:</label>
+                </div>
+                <div class="input">
+                    ${h.text('ldap_dn',class_='small')}
+                </div>
+             </div>
+            
+             <div class="field">
+                <div class="label">
                     <label for="new_password">${_('New password')}:</label>
                 </div>
                 <div class="input">
@@ -231,4 +240,4 @@
         });
 </script>    
 </div>
-</%def>  
\ No newline at end of file
+</%def>
--- a/rhodecode/templates/admin/users/users.html	Tue Feb 01 15:19:42 2011 +0100
+++ b/rhodecode/templates/admin/users/users.html	Thu Feb 03 16:34:40 2011 -0700
@@ -49,7 +49,7 @@
                     <td>${user.last_login}</td>
                     <td>${h.bool2icon(user.active)}</td>
                     <td>${h.bool2icon(user.admin)}</td>
-                    <td>${h.bool2icon(user.is_ldap)}</td>
+                    <td>${h.bool2icon(bool(user.ldap_dn))}</td>
                     <td>
                         ${h.form(url('user', id=user.user_id),method='delete')}
                             ${h.submit('remove_','delete',id="remove_user_%s" % user.user_id,