Changeset - 56c2850a5b5f
[Not reviewed]
beta
0 5 0
Marcin Kuzminski - 15 years ago 2010-11-25 22:58:28
marcin@python-works.com
ldap auth rewrite, moved split authfunc into two functions,
made ldap case insensitive for uids
added some extra debug messages for ldap, and auth function
added some docs for utils
5 files changed with 70 insertions and 49 deletions:
0 comments (0 inline, 0 general)
rhodecode/controllers/login.py
Show inline comments
 
@@ -36,49 +36,49 @@ import formencode
 
import logging
 

	
 
log = logging.getLogger(__name__)
 

	
 
class LoginController(BaseController):
 

	
 
    def __before__(self):
 
        super(LoginController, self).__before__()
 

	
 
    def index(self):
 
        #redirect if already logged in
 
        c.came_from = request.GET.get('came_from', None)
 

	
 
        if c.rhodecode_user.is_authenticated \
 
                            and c.rhodecode_user.username != 'default':
 

	
 
            return redirect(url('home'))
 

	
 
        if request.POST:
 
            #import Login Form validator class
 
            login_form = LoginForm()
 
            try:
 
                c.form_result = login_form.to_python(dict(request.POST))
 
                username = c.form_result['username']
 
                user = UserModel().get_by_username(username)
 
                user = UserModel().get_by_username(username, case_insensitive=True)
 
                auth_user = AuthUser()
 
                auth_user.username = user.username
 
                auth_user.is_authenticated = True
 
                auth_user.is_admin = user.admin
 
                auth_user.user_id = user.user_id
 
                auth_user.name = user.name
 
                auth_user.lastname = user.lastname
 
                session['rhodecode_user'] = auth_user
 
                session.save()
 
                log.info('user %s is now authenticated', username)
 

	
 
                user.update_lastlogin()
 

	
 
                if c.came_from:
 
                    return redirect(c.came_from)
 
                else:
 
                    return redirect(url('home'))
 

	
 
            except formencode.Invalid, errors:
 
                return htmlfill.render(
 
                    render('/login.html'),
 
                    defaults=errors.value,
 
                    errors=errors.error_dict or {},
 
                    prefix_error=False,
rhodecode/lib/auth.py
Show inline comments
 
@@ -56,117 +56,123 @@ class PasswordGenerator(object):
 
    ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM#[5]
 
    ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
 
    ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM#[6]
 
    ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM#[7]
 

	
 
    def __init__(self, passwd=''):
 
        self.passwd = passwd
 

	
 
    def gen_password(self, len, type):
 
        self.passwd = ''.join([random.choice(type) for _ in xrange(len)])
 
        return self.passwd
 

	
 

	
 
def get_crypt_password(password):
 
    """Cryptographic function used for password hashing based on sha1
 
    :param password: password to hash
 
    """
 
    return bcrypt.hashpw(password, bcrypt.gensalt(10))
 

	
 
def check_password(password, hashed):
 
    return bcrypt.hashpw(password, hashed) == hashed
 

	
 
def authfunc(environ, username, password):
 
    """
 
    Authentication function used in Mercurial/Git/ and access control,
 
    Dummy authentication function used in Mercurial/Git/ and access control,
 
    
 
    :param environ: needed only for using in Basic auth
 
    """
 
    return authenticate(username, password)
 

	
 

	
 
def authenticate(username, password):
 
    """
 
    Authentication function used for access control,
 
    firstly checks for db authentication then if ldap is enabled for ldap
 
    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)
 

	
 
    log.debug('Authenticating user using RhodeCode account')
 
    if user is not None and user.is_ldap is False:
 
        if user.active:
 

	
 
            if user.username == 'default' and user.active:
 
                log.info('user %s authenticated correctly', username)
 
                log.info('user %s authenticated correctly as anonymous user',
 
                         username)
 
                return True
 

	
 
            elif user.username == username and check_password(password, user.password):
 
                log.info('user %s authenticated correctly', username)
 
                return True
 
        else:
 
            log.error('user %s is disabled', username)
 

	
 
            log.warning('user %s is disabled', username)
 

	
 
    else:
 

	
 
        #since ldap is searching in case insensitive check if this user is still
 
        #not in our system
 
        username = username.lower()
 
        log.debug('Regular authentication failed')
 
        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:
 
            log.debug('this user already exists as non ldap')
 
            return False
 

	
 
        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):
 

	
 
            log.debug("Authenticating user using ldap")
 
            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)
 
                log.debug('Got ldap response %s', res)
 

	
 
                authenticated = res[1]['uid'][0] == username
 

	
 
                if authenticated and user_model.create_ldap(username, password):
 
                if user_model.create_ldap(username, password):
 
                    log.info('created new ldap user')
 

	
 
                return authenticated
 
            except (LdapUsernameError, LdapPasswordError):
 
                return False
 
            except:
 
                return True
 
            except (LdapUsernameError, LdapPasswordError,):
 
                pass
 
            except (Exception,):
 
                log.error(traceback.format_exc())
 
                return False
 
                pass
 
    return False
 

	
 
class  AuthUser(object):
 
    """
 
    A simple object that handles a mercurial username for authentication
 
    """
 
    def __init__(self):
 
        self.username = 'None'
 
        self.name = ''
 
        self.lastname = ''
 
        self.email = ''
 
        self.user_id = None
 
        self.is_authenticated = False
 
        self.is_admin = False
 
        self.permissions = {}
 

	
 
    def __repr__(self):
 
        return "<AuthUser('id:%s:%s')>" % (self.user_id, self.username)
 

	
 
def set_available_permissions(config):
 
    """
 
    This function will propagate pylons globals with all available defined
 
    permission given in db. We don't wannt to check each time from db for new 
 
    permissions since adding a new permission also requires application restart
rhodecode/lib/utils.py
Show inline comments
 
@@ -107,48 +107,55 @@ def get_repos(path, recursive=False, ini
 
    Scans given path for repos and return (name,(type,path)) tuple 
 
    :param prefix:
 
    :param path:
 
    :param recursive:
 
    :param initial:
 
    """
 
    from vcs.utils.helpers import get_scm
 
    from vcs.exceptions import VCSError
 

	
 
    try:
 
        scm = get_scm(path)
 
    except:
 
        pass
 
    else:
 
        raise Exception('The given path %s should not be a repository got %s',
 
                        path, scm)
 

	
 
    for dirpath in os.listdir(path):
 
        try:
 
            yield dirpath, get_scm(os.path.join(path, dirpath))
 
        except VCSError:
 
            pass
 

	
 
def check_repo_fast(repo_name, base_path):
 
    """
 
    Check given path for existance of directory
 
    :param repo_name:
 
    :param base_path:
 
    
 
    :return False: if this directory is present
 
    """
 
    if os.path.isdir(os.path.join(base_path, repo_name)):return False
 
    return True
 

	
 
def check_repo(repo_name, base_path, verify=True):
 

	
 
    repo_path = os.path.join(base_path, repo_name)
 

	
 
    try:
 
        if not check_repo_fast(repo_name, base_path):
 
            return False
 
        r = hg.repository(ui.ui(), repo_path)
 
        if verify:
 
            hg.verify(r)
 
        #here we hnow that repo exists it was verified
 
        log.info('%s repo is already created', repo_name)
 
        return False
 
    except RepoError:
 
        #it means that there is no valid repo there...
 
        log.info('%s repo is free for creation', repo_name)
 
        return True
 

	
 
def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
 
    while True:
 
        ok = raw_input(prompt)
rhodecode/model/forms.py
Show inline comments
 
""" this is forms validation classes
 
http://formencode.org/module-formencode.validators.html
 
for list off all availible validators
 

	
 
we can create our own validators
 

	
 
The table below outlines the options which can be used in a schema in addition to the validators themselves
 
pre_validators          []     These validators will be applied before the schema
 
chained_validators      []     These validators will be applied after the schema
 
allow_extra_fields      False     If True, then it is not an error when keys that aren't associated with a validator are present
 
filter_extra_fields     False     If True, then keys that aren't associated with a validator are removed
 
if_key_missing          NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
 
ignore_key_missing      False     If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already    
 
  
 
  
 
<name> = formencode.validators.<name of validator>
 
<name> must equal form name
 
list=[1,2,3,4,5]
 
for SELECT use formencode.All(OneOf(list), Int())
 
    
 
"""
 
import os
 
import re
 
import logging
 

	
 
import formencode
 
from formencode import All
 
from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
 
    Email, Bool, StringBoolean
 
from pylons import session
 

	
 
from pylons.i18n.translation import _
 
from rhodecode.lib.auth import authfunc, get_crypt_password
 

	
 
import rhodecode.lib.helpers as h
 
from rhodecode.lib.auth import authenticate, 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
 
from rhodecode.model.db import User
 
from rhodecode import BACKENDS
 

	
 
from webhelpers.pylonslib.secure_form import authentication_token
 
from rhodecode import BACKENDS
 
import formencode
 
import logging
 
import os
 
import re
 
import rhodecode.lib.helpers as h
 

	
 
log = logging.getLogger(__name__)
 

	
 
#this is needed to translate the messages using _() in validators
 
class State_obj(object):
 
    _ = staticmethod(_)
 

	
 
#===============================================================================
 
# VALIDATORS
 
#===============================================================================
 
class ValidAuthToken(formencode.validators.FancyValidator):
 
    messages = {'invalid_token':_('Token mismatch')}
 

	
 
    def validate_python(self, value, state):
 

	
 
        if value != authentication_token():
 
            raise formencode.Invalid(self.message('invalid_token', state,
 
                                            search_number=value), value, state)
 

	
 
def ValidUsername(edit, old_data):
 
    class _ValidUsername(formencode.validators.FancyValidator):
 

	
 
        def validate_python(self, value, state):
 
            if value in ['default', 'new_user']:
 
@@ -121,49 +124,49 @@ class ValidPasswordsMatch(formencode.val
 
    def validate_python(self, value, state):
 

	
 
        if value['password'] != value['password_confirmation']:
 
            e_dict = {'password_confirmation':
 
                   _('Password do not match')}
 
            raise formencode.Invalid('', value, state, error_dict=e_dict)
 

	
 
class ValidAuth(formencode.validators.FancyValidator):
 
    messages = {
 
            'invalid_password':_('invalid password'),
 
            'invalid_login':_('invalid user name'),
 
            'disabled_account':_('Your account is disabled')
 

	
 
            }
 
    #error mapping
 
    e_dict = {'username':messages['invalid_login'],
 
              'password':messages['invalid_password']}
 
    e_dict_disable = {'username':messages['disabled_account']}
 

	
 
    def validate_python(self, value, state):
 
        password = value['password']
 
        username = value['username']
 
        user = UserModel().get_by_username(username)
 

	
 
        if authfunc(None, username, password):
 
        if authenticate(username, password):
 
            return value
 
        else:
 
            if user and user.active is False:
 
                log.warning('user %s is disabled', username)
 
                raise formencode.Invalid(self.message('disabled_account',
 
                                         state=State_obj),
 
                                         value, state,
 
                                         error_dict=self.e_dict_disable)
 
            else:
 
                log.warning('user %s not authenticated', username)
 
                raise formencode.Invalid(self.message('invalid_password',
 
                                         state=State_obj), value, state,
 
                                         error_dict=self.e_dict)
 

	
 
class ValidRepoUser(formencode.validators.FancyValidator):
 

	
 
    def to_python(self, value, state):
 
        sa = meta.Session()
 
        try:
 
            self.user_db = sa.query(User)\
 
                .filter(User.active == True)\
 
                .filter(User.username == value).one()
 
        except Exception:
 
            raise formencode.Invalid(_('This username is not valid'),
rhodecode/model/user.py
Show inline comments
 
#!/usr/bin/env python
 
# encoding: utf-8
 
# Model for users
 
# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
 
# 
 
# -*- coding: utf-8 -*-
 
"""
 
    package.rhodecode.model.user
 
    ~~~~~~~~~~~~~~
 

	
 
    users model for RhodeCode
 
    :created_on: Apr 9, 2010
 
    :author: marcink
 
    :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>    
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software; you can redistribute it and/or
 
# modify it under the terms of the GNU General Public License
 
# as published by the Free Software Foundation; version 2
 
# of the License or (at your opinion) any later version of the license.
 
# 
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
# 
 
# You should have received a copy of the GNU General Public License
 
# along with this program; if not, write to the Free Software
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 
# MA  02110-1301, USA.
 
"""
 
Created on April 9, 2010
 
Model for users
 
:author: marcink
 
"""
 

	
 
from pylons.i18n.translation import _
 
from rhodecode.model import BaseModel
 
from rhodecode.model.caching_query import FromCache
 
from rhodecode.model.db import User
 
from rhodecode.lib.exceptions import *
 

	
 
import logging
 
import traceback
 

	
 
log = logging.getLogger(__name__)
 
from pylons.i18n.translation import _
 

	
 
from rhodecode.model import BaseModel
 
from rhodecode.model.caching_query import FromCache
 
from rhodecode.model.db import User
 

	
 
from rhodecode.lib.exceptions import DefaultUserException, UserOwnsReposException
 

	
 
from sqlalchemy.exc import DatabaseError
 

	
 
log = logging.getLogger(__name__)
 

	
 
class UserModel(BaseModel):
 

	
 
    def get(self, user_id, cache=False):
 
        user = self.sa.query(User)
 
        if cache:
 
            user = user.options(FromCache("sql_cache_short",
 
                                          "get_user_%s" % user_id))
 
        return user.get(user_id)
 

	
 

	
 
    def get_by_username(self, username, cache=False, case_insensitive=False):
 

	
 
        if case_insensitive:
 
            user = self.sa.query(User).filter(User.username.ilike(username))
 
        else:
 
            user = self.sa.query(User)\
 
                .filter(User.username == username)
 
        if cache:
 
            user = user.options(FromCache("sql_cache_short",
 
                                          "get_user_%s" % username))
 
        return user.scalar()
 

	
 
    def create(self, form_data):
 
        try:
 
            new_user = User()
 
            for k, v in form_data.items():
 
                setattr(new_user, k, v)
 

	
 
            self.sa.add(new_user)
 
            self.sa.commit()
 
        except:
 
            log.error(traceback.format_exc())
 
            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:
 
        """
 
        from rhodecode.lib.auth import get_crypt_password
 
        if self.get_by_username(username) is None:
 
        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
 
                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.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:
 
            except (DatabaseError,):
 
                log.error(traceback.format_exc())
 
                self.sa.rollback()
 
                raise
 

	
 
        log.debug('this %s user exists skipping creation of ldap account',
 
                  username)
 
        return False
 

	
 
    def create_registration(self, form_data):
 
        from rhodecode.lib.celerylib import tasks, run_task
 
        try:
 
            new_user = User()
 
            for k, v in form_data.items():
 
                if k != 'admin':
 
                    setattr(new_user, k, v)
 

	
 
            self.sa.add(new_user)
 
            self.sa.commit()
 
            body = ('New user registration\n'
 
                    'username: %s\n'
 
                    'email: %s\n')
 
            body = body % (form_data['username'], form_data['email'])
 

	
 
            run_task(tasks.send_email, None,
 
                     _('[RhodeCode] New User registration'),
 
                     body)
 
        except:
 
            log.error(traceback.format_exc())
 
            self.sa.rollback()
 
            raise
0 comments (0 inline, 0 general)