changeset 705:9e9f1b919c0c beta

implements #60, ldap configuration and authentication. fixes settings to use settings Model
author Marcin Kuzminski <marcin@python-works.com>
date Wed, 17 Nov 2010 22:00:36 +0100
parents 26237de9b613
children 4b354421bf29
files rhodecode/config/routing.py rhodecode/controllers/admin/permissions.py rhodecode/controllers/admin/settings.py rhodecode/lib/auth.py rhodecode/lib/auth_ldap.py rhodecode/model/forms.py rhodecode/model/permission.py rhodecode/model/settings.py rhodecode/model/user.py
diffstat 9 files changed, 248 insertions(+), 92 deletions(-) [+]
line wrap: on
line diff
--- a/rhodecode/config/routing.py	Wed Nov 17 21:29:02 2010 +0100
+++ b/rhodecode/config/routing.py	Wed Nov 17 22:00:36 2010 +0100
@@ -79,6 +79,8 @@
 
     #ADMIN PERMISSIONS REST ROUTES
     map.resource('permission', 'permissions', controller='admin/permissions', path_prefix='/_admin')
+    map.connect('permissions_ldap', '/_admin/permissions_ldap', controller='admin/permissions', action='ldap')
+
 
     #ADMIN SETTINGS REST ROUTES
     with map.submapper(path_prefix='/_admin', controller='admin/settings') as m:
--- a/rhodecode/controllers/admin/permissions.py	Wed Nov 17 21:29:02 2010 +0100
+++ b/rhodecode/controllers/admin/permissions.py	Wed Nov 17 22:00:36 2010 +0100
@@ -29,9 +29,11 @@
 from pylons.i18n.translation import _
 from rhodecode.lib import helpers as h
 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
+from rhodecode.lib.auth_ldap import LdapImportError
 from rhodecode.lib.base import BaseController, render
-from rhodecode.model.forms import UserForm, DefaultPermissionsForm
+from rhodecode.model.forms import LdapSettingsForm, DefaultPermissionsForm
 from rhodecode.model.permission import PermissionModel
+from rhodecode.model.settings import SettingsModel
 from rhodecode.model.user import UserModel
 import formencode
 import logging
@@ -99,17 +101,19 @@
             form_result = _form.to_python(dict(request.POST))
             form_result.update({'perm_user_name':id})
             permission_model.update(form_result)
-            h.flash(_('Default permissions updated succesfully'),
+            h.flash(_('Default permissions updated successfully'),
                     category='success')
 
         except formencode.Invalid, errors:
             c.perms_choices = self.perms_choices
             c.register_choices = self.register_choices
             c.create_choices = self.create_choices
+            defaults = errors.value
+            defaults.update(SettingsModel().get_ldap_settings())
 
             return htmlfill.render(
                 render('admin/permissions/permissions.html'),
-                defaults=errors.value,
+                defaults=defaults,
                 errors=errors.error_dict or {},
                 prefix_error=False,
                 encoding="UTF-8")
@@ -146,6 +150,7 @@
             default_user = UserModel().get_by_username('default')
             defaults = {'_method':'put',
                         'anonymous':default_user.active}
+            defaults.update(SettingsModel().get_ldap_settings())
             for p in default_user.user_perms:
                 if p.permission.permission_name.startswith('repository.'):
                     defaults['default_perm'] = p.permission.permission_name
@@ -163,3 +168,50 @@
                         force_defaults=True,)
         else:
             return redirect(url('admin_home'))
+
+
+    def ldap(self, id_user='default'):
+        """
+        POST ldap create and store ldap settings
+        """
+
+        settings_model = SettingsModel()
+        _form = LdapSettingsForm()()
+
+        try:
+            form_result = _form.to_python(dict(request.POST))
+            try:
+
+                for k, v in form_result.items():
+                    if k.startswith('ldap_'):
+                        setting = settings_model.get(k)
+                        setting.app_settings_value = v
+                        self.sa.add(setting)
+
+                self.sa.commit()
+                h.flash(_('Ldap settings updated successfully'),
+                    category='success')
+            except:
+                raise
+        except LdapImportError:
+            h.flash(_('Unable to activate ldap. The "ldap-python" library '
+                      'is missing.'),
+                    category='warning')
+
+        except formencode.Invalid, errors:
+            c.perms_choices = self.perms_choices
+            c.register_choices = self.register_choices
+            c.create_choices = self.create_choices
+
+            return htmlfill.render(
+                render('admin/permissions/permissions.html'),
+                defaults=errors.value,
+                errors=errors.error_dict or {},
+                prefix_error=False,
+                encoding="UTF-8")
+        except Exception:
+            log.error(traceback.format_exc())
+            h.flash(_('error occured during update of ldap settings'),
+                    category='error')
+
+        return redirect(url('edit_permission', id=id_user))
--- a/rhodecode/controllers/admin/settings.py	Wed Nov 17 21:29:02 2010 +0100
+++ b/rhodecode/controllers/admin/settings.py	Wed Nov 17 22:00:36 2010 +0100
@@ -31,14 +31,15 @@
 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
     HasPermissionAnyDecorator
 from rhodecode.lib.base import BaseController, render
+from rhodecode.lib.celerylib import tasks, run_task
 from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
     set_rhodecode_config, get_hg_settings, get_hg_ui_settings
-from rhodecode.model.db import RhodeCodeSettings, RhodeCodeUi, Repository
+from rhodecode.model.db import RhodeCodeUi, Repository
 from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
     ApplicationUiSettingsForm
 from rhodecode.model.scm import ScmModel
+from rhodecode.model.settings import SettingsModel
 from rhodecode.model.user import UserModel
-from rhodecode.lib.celerylib import tasks, run_task
 from sqlalchemy import func
 import formencode
 import logging
@@ -118,18 +119,12 @@
             application_form = ApplicationSettingsForm()()
             try:
                 form_result = application_form.to_python(dict(request.POST))
-
+                settings_model = SettingsModel()
                 try:
-                    hgsettings1 = self.sa.query(RhodeCodeSettings)\
-                        .filter(RhodeCodeSettings.app_settings_name \
-                                == 'title').one()
-
+                    hgsettings1 = settings_model.get('title')
                     hgsettings1.app_settings_value = form_result['rhodecode_title']
 
-                    hgsettings2 = self.sa.query(RhodeCodeSettings)\
-                        .filter(RhodeCodeSettings.app_settings_name \
-                                == 'realm').one()
-
+                    hgsettings2 = settings_model('realm')
                     hgsettings2.app_settings_value = form_result['rhodecode_realm']
 
 
--- a/rhodecode/lib/auth.py	Wed Nov 17 21:29:02 2010 +0100
+++ b/rhodecode/lib/auth.py	Wed Nov 17 22:00:36 2010 +0100
@@ -25,6 +25,7 @@
 from pylons import config, session, url, request
 from pylons.controllers.util import abort, redirect
 from rhodecode.lib.utils import get_repo_slug
+from rhodecode.lib.auth_ldap import AuthLdap, UsernameError, PasswordError
 from rhodecode.model import meta
 from rhodecode.model.user import UserModel
 from rhodecode.model.caching_query import FromCache
@@ -34,6 +35,7 @@
 from decorator import decorator
 import logging
 import random
+import traceback
 
 log = logging.getLogger(__name__)
 
@@ -74,17 +76,18 @@
 
 def authfunc(environ, username, password):
     """
-    Authentication function used in Mercurial/Git/ and access controll,
+    Authentication function used in Mercurial/Git/ and access control,
     firstly checks for db authentication then if ldap is enabled for ldap
-    authentication
+    authentication, also creates ldap user if not in database
+    
     :param environ: needed only for using in Basic auth, can be None
     :param username: username
     :param password: password
     """
+    user_model = UserModel()
+    user = user_model.get_by_username(username, cache=False)
 
-    user = UserModel().get_by_username(username, cache=False)
-
-    if user:
+    if user is not None and user.is_ldap is False:
         if user.active:
 
             if user.username == 'default' and user.active:
@@ -97,6 +100,40 @@
         else:
             log.error('user %s is disabled', username)
 
+
+    else:
+        from rhodecode.model.settings import SettingsModel
+        ldap_settings = SettingsModel().get_ldap_settings()
+
+        #======================================================================
+        # FALLBACK TO LDAP AUTH IN ENABLE                
+        #======================================================================
+        if ldap_settings.get('ldap_active', False):
+            kwargs = {
+                  'server':ldap_settings.get('ldap_host', ''),
+                  'base_dn':ldap_settings.get('ldap_base_dn', ''),
+                  'port':ldap_settings.get('ldap_port'),
+                  'bind_dn':ldap_settings.get('ldap_dn_user'),
+                  'bind_pass':ldap_settings.get('ldap_dn_pass'),
+                  'use_ldaps':ldap_settings.get('ldap_ldaps'),
+                  'ldap_version':3,
+                  }
+            log.debug('Checking for ldap authentication')
+            try:
+                aldap = AuthLdap(**kwargs)
+                res = aldap.authenticate_ldap(username, password)
+
+                authenticated = res[1]['uid'][0] == username
+
+                if authenticated and user_model.create_ldap(username, password):
+                    log.info('created new ldap user')
+
+                return authenticated
+            except (UsernameError, PasswordError):
+                return False
+            except:
+                log.error(traceback.format_exc())
+                return False
     return False
 
 class  AuthUser(object):
--- a/rhodecode/lib/auth_ldap.py	Wed Nov 17 21:29:02 2010 +0100
+++ b/rhodecode/lib/auth_ldap.py	Wed Nov 17 22:00:36 2010 +0100
@@ -1,7 +1,3 @@
-import logging
-logging.basicConfig(level=logging.DEBUG)
-log = logging.getLogger('ldap')
-
 #==============================================================================
 # LDAP
 #Name     = Just a description for the auth modes page
@@ -11,76 +7,87 @@
 #Account  = DepartmentName\UserName (or UserName@MyDomain depending on AD server)
 #Password = <password>
 #Base DN  = DC=DepartmentName,DC=OrganizationName,DC=local
-#
-#On-the-fly user creation = yes
-#Attributes
-#  Login     = sAMAccountName
-#  Firstname = givenName
-#  Lastname  = sN
-#  Email     = mail
 
 #==============================================================================
-class UsernameError(Exception):pass
-class PasswordError(Exception):pass
+
+from rhodecode.lib.exceptions import LdapImportError, UsernameError, \
+    PasswordError, ConnectionError
+import logging
+
+log = logging.getLogger(__name__)
 
-LDAP_USE_LDAPS = False
-ldap_server_type = 'ldap'
-LDAP_SERVER_ADDRESS = 'myldap.com'
-LDAP_SERVER_PORT = '389'
+try:
+    import ldap
+except ImportError:
+    pass
 
-#USE FOR READ ONLY BIND TO LDAP SERVER
-LDAP_BIND_DN = ''
-LDAP_BIND_PASS = ''
+class AuthLdap(object):
 
-if LDAP_USE_LDAPS:ldap_server_type = ldap_server_type + 's'
-LDAP_SERVER = "%s://%s:%s" % (ldap_server_type,
-                                       LDAP_SERVER_ADDRESS,
-                                       LDAP_SERVER_PORT)
-
-BASE_DN = "ou=people,dc=server,dc=com"
-AUTH_DN = "uid=%s,%s"
+    def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
+                 use_ldaps=False, ldap_version=3):
+        self.ldap_version = ldap_version
+        if use_ldaps:
+            port = port or 689
+        self.LDAP_USE_LDAPS = use_ldaps
+        self.LDAP_SERVER_ADDRESS = server
+        self.LDAP_SERVER_PORT = port
 
-def authenticate_ldap(username, password):
-    """Authenticate a user via LDAP and return his/her LDAP properties.
+        #USE FOR READ ONLY BIND TO LDAP SERVER
+        self.LDAP_BIND_DN = bind_dn
+        self.LDAP_BIND_PASS = bind_pass
 
-    Raises AuthenticationError if the credentials are rejected, or
-    EnvironmentError if the LDAP server can't be reached.
-    """
-    try:
-        import ldap
-    except ImportError:
-        raise Exception('Could not import ldap make sure You install python-ldap')
+        ldap_server_type = 'ldap'
+        if self.LDAP_USE_LDAPS:ldap_server_type = ldap_server_type + 's'
+        self.LDAP_SERVER = "%s://%s:%s" % (ldap_server_type,
+                                               self.LDAP_SERVER_ADDRESS,
+                                               self.LDAP_SERVER_PORT)
+
+        self.BASE_DN = base_dn
+        self.AUTH_DN = "uid=%s,%s"
 
-    from rhodecode.lib.helpers import chop_at
-
-    uid = chop_at(username, "@%s" % LDAP_SERVER_ADDRESS)
-    dn = AUTH_DN % (uid, BASE_DN)
-    log.debug("Authenticating %r at %s", dn, LDAP_SERVER)
-    if "," in username:
-        raise UsernameError("invalid character in username: ,")
-    try:
-        #ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, '/etc/openldap/cacerts')
-        server = ldap.initialize(LDAP_SERVER)
-        server.protocol = ldap.VERSION3
-        
-        if LDAP_BIND_DN and LDAP_BIND_PASS:
-            server.simple_bind_s(AUTH_DN % (LDAP_BIND_DN,
-                                            LDAP_BIND_PASS),
-                                            password)
+    def authenticate_ldap(self, username, password):
+        """Authenticate a user via LDAP and return his/her LDAP properties.
+    
+        Raises AuthenticationError if the credentials are rejected, or
+        EnvironmentError if the LDAP server can't be reached.
         
-        server.simple_bind_s(dn, password)
-        properties = server.search_s(dn, ldap.SCOPE_SUBTREE)
-        if not properties:
-            raise ldap.NO_SUCH_OBJECT()
-    except ldap.NO_SUCH_OBJECT, e:
-        log.debug("LDAP says no such user '%s' (%s)", uid, username)
-        raise UsernameError()
-    except ldap.INVALID_CREDENTIALS, e:
-        log.debug("LDAP rejected password for user '%s' (%s)", uid, username)
-        raise PasswordError()
-    except ldap.SERVER_DOWN, e:
-        raise EnvironmentError("can't access authentication server")
-    return properties
+        :param username: username
+        :param password: password
+        """
+
+        from rhodecode.lib.helpers import chop_at
+
+        uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS)
+        dn = self.AUTH_DN % (uid, self.BASE_DN)
+        log.debug("Authenticating %r at %s", dn, self.LDAP_SERVER)
+        if "," in username:
+            raise UsernameError("invalid character in username: ,")
+        try:
+            ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, '/etc/openldap/cacerts')
+            ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
+            server = ldap.initialize(self.LDAP_SERVER)
+            if self.ldap_version == 2:
+                server.protocol = ldap.VERSION2
+            else:
+                server.protocol = ldap.VERSION3
 
+            if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
+                server.simple_bind_s(self.AUTH_DN % (self.LDAP_BIND_DN,
+                                                self.BASE_DN),
+                                                self.LDAP_BIND_PASS)
 
-print authenticate_ldap('test', 'test')
+            server.simple_bind_s(dn, password)
+            properties = server.search_s(dn, ldap.SCOPE_SUBTREE)
+            if not properties:
+                raise ldap.NO_SUCH_OBJECT()
+        except ldap.NO_SUCH_OBJECT, e:
+            log.debug("LDAP says no such user '%s' (%s)", uid, username)
+            raise UsernameError()
+        except ldap.INVALID_CREDENTIALS, e:
+            log.debug("LDAP rejected password for user '%s' (%s)", uid, username)
+            raise PasswordError()
+        except ldap.SERVER_DOWN, e:
+            raise ConnectionError("LDAP can't access authentication server")
+
+        return properties[0]
+
--- a/rhodecode/model/forms.py	Wed Nov 17 21:29:02 2010 +0100
+++ b/rhodecode/model/forms.py	Wed Nov 17 22:00:36 2010 +0100
@@ -25,6 +25,7 @@
 from pylons import session
 from pylons.i18n.translation import _
 from rhodecode.lib.auth import authfunc, get_crypt_password
+from rhodecode.lib.exceptions import LdapImportError
 from rhodecode.model import meta
 from rhodecode.model.user import UserModel
 from rhodecode.model.repo import RepoModel
@@ -82,7 +83,7 @@
     messages = {
             'invalid_password':_('invalid password'),
             'invalid_login':_('invalid user name'),
-            'disabled_account':_('Your acccount is disabled')
+            'disabled_account':_('Your account is disabled')
 
             }
     #error mapping
@@ -236,6 +237,16 @@
 
         return value
 
+class LdapLibValidator(formencode.validators.FancyValidator):
+
+    def to_python(self, value, state):
+
+        try:
+            import ldap
+        except ImportError:
+            raise LdapImportError
+        return value
+
 #===============================================================================
 # FORMS        
 #===============================================================================
@@ -352,10 +363,26 @@
     class _DefaultPermissionsForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
-        overwrite_default = OneOf(['true', 'false'], if_missing='false')
+        overwrite_default = StringBoolean(if_missing=False)
         anonymous = OneOf(['True', 'False'], if_missing=False)
         default_perm = OneOf(perms_choices)
         default_register = OneOf(register_choices)
         default_create = OneOf(create_choices)
 
     return _DefaultPermissionsForm
+
+
+def LdapSettingsForm():
+    class _LdapSettingsForm(formencode.Schema):
+        allow_extra_fields = True
+        filter_extra_fields = True
+        pre_validators = [LdapLibValidator]
+        ldap_active = StringBoolean(if_missing=False)
+        ldap_host = UnicodeString(strip=True,)
+        ldap_port = Number(strip=True,)
+        ldap_ldaps = StringBoolean(if_missing=False)
+        ldap_dn_user = UnicodeString(strip=True,)
+        ldap_dn_pass = UnicodeString(strip=True,)
+        ldap_base_dn = UnicodeString(strip=True,)
+
+    return _LdapSettingsForm
--- a/rhodecode/model/permission.py	Wed Nov 17 21:29:02 2010 +0100
+++ b/rhodecode/model/permission.py	Wed Nov 17 22:00:36 2010 +0100
@@ -96,8 +96,3 @@
             log.error(traceback.format_exc())
             self.sa.rollback()
             raise
-
-
-
-
-
--- a/rhodecode/model/settings.py	Wed Nov 17 21:29:02 2010 +0100
+++ b/rhodecode/model/settings.py	Wed Nov 17 22:00:36 2010 +0100
@@ -51,6 +51,17 @@
 
 
     def get_ldap_settings(self):
+        """
+        Returns ldap settings from database
+        :returns:
+        ldap_active
+        ldap_host
+        ldap_port 
+        ldap_ldaps
+        ldap_dn_user 
+        ldap_dn_pass 
+        ldap_base_dn
+        """
 
         r = self.sa.query(RhodeCodeSettings)\
                 .filter(RhodeCodeSettings.app_settings_name\
--- a/rhodecode/model/user.py	Wed Nov 17 21:29:02 2010 +0100
+++ b/rhodecode/model/user.py	Wed Nov 17 22:00:36 2010 +0100
@@ -68,6 +68,36 @@
             self.sa.rollback()
             raise
 
+    def create_ldap(self, username, password):
+        """
+        Checks if user is in database, if not creates this user marked
+        as ldap user
+        :param username:
+        :param password:
+        """
+
+        if self.get_by_username(username) is None:
+            try:
+                new_user = User()
+                new_user.username = username
+                new_user.password = password
+                new_user.email = '%s@ldap.server' % username
+                new_user.active = True
+                new_user.is_ldap = True
+                new_user.name = '%s@ldap' % username
+                new_user.lastname = ''
+
+
+                self.sa.add(new_user)
+                self.sa.commit()
+                return True
+            except:
+                log.error(traceback.format_exc())
+                self.sa.rollback()
+                raise
+
+        return False
+
     def create_registration(self, form_data):
         from rhodecode.lib.celerylib import tasks, run_task
         try: