Changeset - 04e8b31fb245
[Not reviewed]
default
0 3 0
Marcin Kuzminski - 15 years ago 2010-08-20 10:59:18
marcin@python-works.com
Changed password crypting scheme to bcrypt, added dependency for setup
3 files changed with 11 insertions and 9 deletions:
0 comments (0 inline, 0 general)
pylons_app/lib/auth.py
Show inline comments
 
#!/usr/bin/env python
 
# encoding: utf-8
 
# authentication and permission libraries
 
# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
 
#
 
# 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 4, 2010
 

	
 
@author: marcink
 
"""
 
from beaker.cache import cache_region
 
from pylons import config, session, url, request
 
from pylons.controllers.util import abort, redirect
 
from pylons_app.lib.utils import get_repo_slug
 
from pylons_app.model import meta
 
from pylons_app.model.db import User, RepoToPerm, Repository, Permission
 
from sqlalchemy.exc import OperationalError
 
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
 
import hashlib
 
import bcrypt
 
from decorator import decorator
 
import logging
 

	
 
log = logging.getLogger(__name__) 
 

	
 
def get_crypt_password(password):
 
    """Cryptographic function used for password hashing based on sha1
 
    @param password: password to hash
 
    """
 
    hashed = hashlib.sha1(password).hexdigest()
 
    return hashed[3:] + hashed[:3]
 
    """    
 
    return bcrypt.hashpw(password, bcrypt.gensalt(10))
 

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

	
 
@cache_region('super_short_term', 'cached_user')
 
def get_user_cached(username):
 
    sa = meta.Session
 
    try:
 
        user = sa.query(User).filter(User.username == username).one()
 
    finally:
 
        meta.Session.remove()
 
    return user
 

	
 
def authfunc(environ, username, password):
 
    password_crypt = get_crypt_password(password)
 
    try:
 
        user = get_user_cached(username)
 
    except (NoResultFound, MultipleResultsFound, OperationalError) as e:
 
        log.error(e)
 
        user = None
 
        
 
    if user:
 
        if user.active:
 
            if user.username == username and user.password == password_crypt:
 
            if 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)
 
            
 
    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 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
 
    ie. to decorate new views with the newly created permission
 
    @param config:
 
    """
 
    log.info('getting information about all available permissions')
 
    try:
 
        sa = meta.Session
 
        all_perms = sa.query(Permission).all()
 
    finally:
 
        meta.Session.remove()
 
    
 
    config['available_permissions'] = [x.permission_name for x in all_perms]
 

	
 
def set_base_path(config):
 
    config['base_path'] = config['pylons.app_globals'].base_path
 

	
 
def fill_data(user):
 
    """
 
    Fills user data with those from database and log out user if not present
 
    in database
 
    @param user:
 
    """
 
    sa = meta.Session
 
    dbuser = sa.query(User).get(user.user_id)
 
    if dbuser:
 
        user.username = dbuser.username
 
        user.is_admin = dbuser.admin
 
        user.name = dbuser.name
 
        user.lastname = dbuser.lastname
 
        user.email = dbuser.email
 
    else:
 
        user.is_authenticated = False
 
    meta.Session.remove()
 
    return user
 
            
 
def fill_perms(user):
 
    """
 
    Fills user permission attribute with permissions taken from database
 
    @param user:
 
    """
 
    
 
    sa = meta.Session
 
    user.permissions['repositories'] = {}
 
    user.permissions['global'] = set()
 
    
 
    #first fetch default permissions
 
    default_perms = sa.query(RepoToPerm, Repository, Permission)\
 
        .join((Repository, RepoToPerm.repository_id == Repository.repo_id))\
 
        .join((Permission, RepoToPerm.permission_id == Permission.permission_id))\
 
        .filter(RepoToPerm.user_id == sa.query(User).filter(User.username == 
 
                                            'default').one().user_id).all()
 

	
 
    if user.is_admin:
 
        user.permissions['global'].add('hg.admin')
 
        #admin have all rights set to admin
 
        for perm in default_perms:
 
            p = 'repository.admin'
 
            user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p
 
    
 
    else:
 
        user.permissions['global'].add('repository.create')
 
        user.permissions['global'].add('hg.register')
 
        
 
        for perm in default_perms:
 
            if perm.Repository.private and not perm.Repository.user_id == user.user_id:
 
                #disable defaults for private repos,
 
                p = 'repository.none'
 
            elif perm.Repository.user_id == user.user_id:
 
                #set admin if owner
 
                p = 'repository.admin'
pylons_app/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())
 
    
 
"""
 
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 pylons_app.lib.auth import get_crypt_password
 
from pylons_app.lib.auth import check_password
 
from pylons_app.model import meta
 
from pylons_app.model.db import User, Repository
 
from sqlalchemy.exc import OperationalError
 
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
 
from webhelpers.pylonslib.secure_form import authentication_token
 
import datetime
 
import formencode
 
import logging
 
import os
 
import pylons_app.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']:
 
                raise formencode.Invalid(_('Invalid username'), value, state)
 
            #check if user is uniq
 
            sa = meta.Session
 
            old_un = None
 
            if edit:
 
                old_un = sa.query(User).get(old_data.get('user_id')).username
 
                
 
            if old_un != value or not edit:    
 
                if sa.query(User).filter(User.username == value).scalar():
 
                    raise formencode.Invalid(_('This username already exists') ,
 
                                             value, state)
 
            meta.Session.remove()
 
                            
 
    return _ValidUsername   
 
    
 
class ValidPassword(formencode.validators.FancyValidator):
 
    
 
    def to_python(self, value, state):
 
        if value:
 
            return get_crypt_password(value)
 
        
 
class ValidAuth(formencode.validators.FancyValidator):
 
    messages = {
 
            'invalid_password':_('invalid password'),
 
            'invalid_login':_('invalid user name'),
 
            'disabled_account':_('Your acccount 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):
 
        sa = meta.Session
 
        crypted_passwd = get_crypt_password(value['password'])
 
        password = value['password']
 
        username = value['username']
 
        try:
 
            user = sa.query(User).filter(User.username == username).one()
 
        except (NoResultFound, MultipleResultsFound, OperationalError) as e:
 
            log.error(e)
 
            user = None
 
            raise formencode.Invalid(self.message('invalid_password',
 
                                     state=State_obj), value, state,
 
                                     error_dict=self.e_dict)            
 
        if user:
 
            if user.active:
 
                if user.username == username and user.password == crypted_passwd:
 
                if user.username == username and check_password(password, user.password):
 
                    from pylons_app.lib.auth import AuthUser
 
                    auth_user = AuthUser()
 
                    auth_user.username = 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['hg_app_user'] = auth_user
 
                    session.save()
 
                    log.info('user %s is now authenticated', username)
 
                    
 
                    try:
 
                        user.last_login = datetime.datetime.now()
 
                        sa.add(user)
 
                        sa.commit()                        
 
                    except (OperationalError) as e:
 
                        log.error(e)
 
                        sa.rollback()
 
                    
 
                    return value
 
                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)
 
            else:
 
                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)
 
            
 
        meta.Session.remove()
 

	
 
                   
 
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'),
 
                                     value, state)
 
        meta.Session.remove()            
 
        return self.user_db.user_id
 

	
 
def ValidRepoName(edit, old_data):    
 
    class _ValidRepoName(formencode.validators.FancyValidator):
 
            
 
        def to_python(self, value, state):
 
            slug = h.repo_name_slug(value)
 
            if slug in ['_admin']:
 
                raise formencode.Invalid(_('This repository name is disallowed'),
 
                                         value, state)
 
            if old_data.get('repo_name') != value or not edit:    
 
                sa = meta.Session
 
                if sa.query(Repository).filter(Repository.repo_name == slug).scalar():
 
                    raise formencode.Invalid(_('This repository already exists') ,
 
                                             value, state)
 
                meta.Session.remove()
 
            return slug 
 
        
 
        
 
    return _ValidRepoName
 

	
 
class ValidPerms(formencode.validators.FancyValidator):
 
    messages = {'perm_new_user_name':_('This username is not valid')}
 
    
 
    def to_python(self, value, state):
 
        perms_update = []
 
        perms_new = []
 
        #build a list of permission to update and new permission to create
 
        for k, v in value.items():
 
            if k.startswith('perm_'):
 
                if  k.startswith('perm_new_user'):
 
                    new_perm = value.get('perm_new_user', False)
 
                    new_user = value.get('perm_new_user_name', False)
 
                    if new_user and new_perm:
 
                        if (new_user, new_perm) not in perms_new:
 
                            perms_new.append((new_user, new_perm))
 
                else:
 
                    usr = k[5:]                    
 
                    if usr == 'default':
 
                        if value['private']:
 
                            #set none for default when updating to private repo
 
                            v = 'repository.none'
 
                    perms_update.append((usr, v))
 
        value['perms_updates'] = perms_update
 
        value['perms_new'] = perms_new
 
        sa = meta.Session
 
        for k, v in perms_new:
 
            try:
setup.py
Show inline comments
 
from pylons_app import get_version
 
try:
 
    from setuptools import setup, find_packages
 
except ImportError:
 
    from ez_setup import use_setuptools
 
    use_setuptools()
 
    from setuptools import setup, find_packages
 

	
 
setup(
 
    name='hg_app',
 
    version=get_version(),
 
    description='Mercurial repository serving and browsing app',
 
    keywords='mercurial web hgwebdir replacement serving hgweb',
 
    license='BSD',
 
    author='marcin kuzminski',
 
    author_email='marcin@python-works.com',
 
    url='http://hg.python-works.com',
 
    install_requires=[
 
        "Pylons>=1.0.0",
 
        "SQLAlchemy>=0.6",
 
        "Mako>=0.3.2",
 
        "vcs>=0.1.4",
 
        "pygments>=1.3.0",
 
        "mercurial>=1.6",
 
        "pysqlite",
 
        "whoosh>=1.0.0b5",
 
        "py-bcrypt",
 
    ],
 
    setup_requires=["PasteScript>=1.6.3"],
 
    packages=find_packages(exclude=['ez_setup']),
 
    include_package_data=True,
 
    test_suite='nose.collector',
 
    package_data={'pylons_app': ['i18n/*/LC_MESSAGES/*.mo']},
 
    message_extractors={'pylons_app': [
 
            ('**.py', 'python', None),
 
            ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
 
            ('public/**', 'ignore', None)]},
 
    zip_safe=False,
 
    paster_plugins=['PasteScript', 'Pylons'],
 
    entry_points="""
 
    [paste.app_factory]
 
    main = pylons_app.config.middleware:make_app
 

	
 
    [paste.app_install]
 
    main = pylons.util:PylonsInstaller
 
    """,
 
)
0 comments (0 inline, 0 general)