# HG changeset patch # User Marcin Kuzminski # Date 2010-11-17 21:00:36 # Node ID 9e9f1b919c0c151b212c51f98daa2a3ae1183be8 # Parent 26237de9b61350b17ac585981ad4ab2cab78f717 implements #60, ldap configuration and authentication. fixes settings to use settings Model diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -79,6 +79,8 @@ def make_map(config): #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: diff --git a/rhodecode/controllers/admin/permissions.py b/rhodecode/controllers/admin/permissions.py --- a/rhodecode/controllers/admin/permissions.py +++ b/rhodecode/controllers/admin/permissions.py @@ -29,9 +29,11 @@ from pylons.controllers.util import abor 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 @@ class PermissionsController(BaseControll 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 @@ class PermissionsController(BaseControll 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 @@ class PermissionsController(BaseControll 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)) diff --git a/rhodecode/controllers/admin/settings.py b/rhodecode/controllers/admin/settings.py --- a/rhodecode/controllers/admin/settings.py +++ b/rhodecode/controllers/admin/settings.py @@ -31,14 +31,15 @@ from rhodecode.lib import helpers as h 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 @@ class SettingsController(BaseController) 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'] diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -25,6 +25,7 @@ Created on April 4, 2010 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 @@ import bcrypt from decorator import decorator import logging import random +import traceback log = logging.getLogger(__name__) @@ -74,17 +76,18 @@ def check_password(password, hashed): 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 @@ def authfunc(environ, username, password 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): 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 @@ -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 @@ log = logging.getLogger('ldap') #Account = DepartmentName\UserName (or UserName@MyDomain depending on AD server) #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] + diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -25,6 +25,7 @@ from formencode.validators import Unicod 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 @@ class ValidAuth(formencode.validators.Fa 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 @@ class ValidSystemEmail(formencode.valida 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 @@ def DefaultPermissionsForm(perms_choices 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 diff --git a/rhodecode/model/permission.py b/rhodecode/model/permission.py --- a/rhodecode/model/permission.py +++ b/rhodecode/model/permission.py @@ -96,8 +96,3 @@ class PermissionModel(object): log.error(traceback.format_exc()) self.sa.rollback() raise - - - - - diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -51,6 +51,17 @@ class SettingsModel(object): 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\ diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -68,6 +68,36 @@ class UserModel(object): 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: