# HG changeset patch # User Thayne Harbaugh # Date 2011-02-03 23:34:40 # Node ID b232a36cc51f8acfd59c464b5961e59f782fa873 # Parent 7a1df0130533de6d5c52d28adffe53bec5b4fe99 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. diff --git a/rhodecode/controllers/admin/ldap_settings.py b/rhodecode/controllers/admin/ldap_settings.py --- a/rhodecode/controllers/admin/ldap_settings.py +++ b/rhodecode/controllers/admin/ldap_settings.py @@ -48,15 +48,33 @@ log = logging.getLogger(__name__) 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 @@ class LdapSettingsController(BaseControl """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 @@ class LdapSettingsController(BaseControl 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, diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -103,7 +103,7 @@ def authenticate(username, password): 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 @@ def authenticate(username, password): 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 @@ def authenticate(username, password): '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 diff --git a/rhodecode/lib/auth_ldap.py b/rhodecode/lib/auth_ldap.py --- a/rhodecode/lib/auth_ldap.py +++ b/rhodecode/lib/auth_ldap.py @@ -36,11 +36,15 @@ except ImportError: 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 @@ class AuthLdap(object): 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 @@ class AuthLdap(object): 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 @@ class AuthLdap(object): 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) diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -336,7 +336,10 @@ class DbManage(object): 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) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -105,7 +105,7 @@ class User(Base, BaseModel): 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') diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -334,23 +334,14 @@ class LdapLibValidator(formencode.valida 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 @@ def DefaultPermissionsForm(perms_choices 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 @@ def LdapSettingsForm(): 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 diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -72,10 +72,18 @@ class SettingsModel(BaseModel): 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\ diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -75,25 +75,27 @@ class UserModel(BaseModel): 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) diff --git a/rhodecode/templates/admin/ldap/ldap.html b/rhodecode/templates/admin/ldap/ldap.html --- a/rhodecode/templates/admin/ldap/ldap.html +++ b/rhodecode/templates/admin/ldap/ldap.html @@ -21,13 +21,13 @@
${self.breadcrumbs()}
-

${_('LDAP administration')}

${h.form(url('ldap_settings'))}
+

${_('Connection settings')}

-
+
${h.checkbox('ldap_active',True,class_='small')}
@@ -39,10 +39,6 @@
${h.text('ldap_port',class_='small')}
-
-
${h.checkbox('ldap_ldaps',True,class_='small')}
-
-
${h.text('ldap_dn_user',class_='small')}
@@ -51,9 +47,43 @@
${h.password('ldap_dn_pass',class_='small')}
+
+
${h.checkbox('ldap_ldaps',True,class_='small')}
+
+
+
+
${h.select('ldap_tls_reqcert',c.tls_reqcert_cur,c.tls_reqcert_choices,class_='small')}
+
+

${_('Search settings')}

+
${h.text('ldap_base_dn',class_='small')}
+
+
+
${h.text('ldap_filter',class_='small')}
+
+
+
+
${h.select('ldap_search_scope',c.search_scope_cur,c.search_scope_choices,class_='small')}
+
+

${_('Attribute mappings')}

+
+
+
${h.text('ldap_attr_login',class_='small')}
+
+
+
+
${h.text('ldap_attr_firstname',class_='small')}
+
+
+
+
${h.text('ldap_attr_lastname',class_='small')}
+
+
+
+
${h.text('ldap_attr_email',class_='small')}
+
${h.submit('save','Save',class_="ui-button")} diff --git a/rhodecode/templates/admin/users/user_edit.html b/rhodecode/templates/admin/users/user_edit.html --- a/rhodecode/templates/admin/users/user_edit.html +++ b/rhodecode/templates/admin/users/user_edit.html @@ -49,6 +49,15 @@
+ +
+
+ ${h.text('ldap_dn',class_='small')} +
+
+ +
+
@@ -231,4 +240,4 @@ });
- \ No newline at end of file + diff --git a/rhodecode/templates/admin/users/users.html b/rhodecode/templates/admin/users/users.html --- a/rhodecode/templates/admin/users/users.html +++ b/rhodecode/templates/admin/users/users.html @@ -49,7 +49,7 @@ ${user.last_login} ${h.bool2icon(user.active)} ${h.bool2icon(user.admin)} - ${h.bool2icon(user.is_ldap)} + ${h.bool2icon(bool(user.ldap_dn))} ${h.form(url('user', id=user.user_id),method='delete')} ${h.submit('remove_','delete',id="remove_user_%s" % user.user_id,