diff --git a/rhodecode/controllers/admin/users.py b/rhodecode/controllers/admin/users.py --- a/rhodecode/controllers/admin/users.py +++ b/rhodecode/controllers/admin/users.py @@ -36,8 +36,7 @@ from pylons.i18n.translation import _ from rhodecode.lib.exceptions import DefaultUserException, UserOwnsReposException from rhodecode.lib import helpers as h -from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \ - fill_perms +from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator from rhodecode.lib.base import BaseController, render from rhodecode.model.db import User @@ -157,14 +156,15 @@ class UsersController(BaseController): def edit(self, id, format='html'): """GET /users/id/edit: Form to edit an existing item""" # url('edit_user', id=ID) - c.user = self.sa.query(User).get(id) + user_model = UserModel() + c.user = user_model.get(id) if not c.user: return redirect(url('users')) if c.user.username == 'default': h.flash(_("You can't edit this user"), category='warning') return redirect(url('users')) c.user.permissions = {} - c.granted_permissions = fill_perms(c.user).permissions['global'] + c.granted_permissions = user_model.fill_perms(c.user).permissions['global'] defaults = c.user.get_dict() diff --git a/rhodecode/controllers/admin/users_groups.py b/rhodecode/controllers/admin/users_groups.py --- a/rhodecode/controllers/admin/users_groups.py +++ b/rhodecode/controllers/admin/users_groups.py @@ -36,8 +36,7 @@ from pylons.i18n.translation import _ from rhodecode.lib.exceptions import DefaultUserException, UserOwnsReposException from rhodecode.lib import helpers as h -from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \ - fill_perms +from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator from rhodecode.lib.base import BaseController, render from rhodecode.model.db import User, UsersGroup diff --git a/rhodecode/controllers/journal.py b/rhodecode/controllers/journal.py --- a/rhodecode/controllers/journal.py +++ b/rhodecode/controllers/journal.py @@ -49,6 +49,7 @@ class JournalController(BaseController): @LoginRequired() def __before__(self): super(JournalController, self).__before__() + c.rhodecode_user = self.rhodecode_user self.title = _('%s public journal %s feed') % (c.rhodecode_name, '%s') self.language = 'en-us' self.ttl = "5" @@ -60,7 +61,7 @@ class JournalController(BaseController): p = int(request.params.get('page', 1)) c.following = self.sa.query(UserFollowing)\ - .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ + .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\ .options(joinedload(UserFollowing.follows_repository))\ .all() @@ -126,7 +127,7 @@ class JournalController(BaseController): if user_id: try: self.scm_model.toggle_following_user(user_id, - c.rhodecode_user.user_id) + self.rhodecode_user.user_id) return 'ok' except: raise HTTPInternalServerError() @@ -135,7 +136,7 @@ class JournalController(BaseController): if repo_id: try: self.scm_model.toggle_following_repo(repo_id, - c.rhodecode_user.user_id) + self.rhodecode_user.user_id) return 'ok' except: raise HTTPInternalServerError() @@ -152,7 +153,7 @@ class JournalController(BaseController): p = int(request.params.get('page', 1)) c.following = self.sa.query(UserFollowing)\ - .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ + .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\ .options(joinedload(UserFollowing.follows_repository))\ .all() @@ -174,7 +175,7 @@ class JournalController(BaseController): Produce an atom-1.0 feed via feedgenerator module """ c.following = self.sa.query(UserFollowing)\ - .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ + .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\ .options(joinedload(UserFollowing.follows_repository))\ .all() @@ -207,7 +208,7 @@ class JournalController(BaseController): Produce an rss2 feed via feedgenerator module """ c.following = self.sa.query(UserFollowing)\ - .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ + .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\ .options(joinedload(UserFollowing.follows_repository))\ .all() diff --git a/rhodecode/controllers/login.py b/rhodecode/controllers/login.py --- a/rhodecode/controllers/login.py +++ b/rhodecode/controllers/login.py @@ -62,19 +62,17 @@ class LoginController(BaseController): login_form = LoginForm() try: c.form_result = login_form.to_python(dict(request.POST)) + #form checks for username/password, now we're authenticated username = c.form_result['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 + user = UserModel().get_by_username(username, + case_insensitive=True) + auth_user = AuthUser(user.user_id) + auth_user.set_authenticated() session['rhodecode_user'] = auth_user session.save() - log.info('user %s is now authenticated', username) + log.info('user %s is now authenticated and stored in session', + username) user.update_lastlogin() if c.came_from: @@ -146,7 +144,7 @@ class LoginController(BaseController): return render('/password_reset.html') def logout(self): - session['rhodecode_user'] = AuthUser() + del session['rhodecode_user'] session.save() log.info('Logging out and setting user as Empty') redirect(url('home')) diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -42,19 +42,11 @@ from rhodecode.lib.auth_ldap import Auth from rhodecode.model import meta from rhodecode.model.user import UserModel -from rhodecode.model.db import User, RepoToPerm, Repository, Permission, \ - UserToPerm, UsersGroupToPerm, UsersGroupMember +from rhodecode.model.db import Permission log = logging.getLogger(__name__) - -PERM_WEIGHTS = {'repository.none':0, - 'repository.read':1, - 'repository.write':3, - 'repository.admin':3} - - class PasswordGenerator(object): """This is a simple class for generating password from different sets of characters @@ -185,21 +177,66 @@ def authenticate(username, password): return False class AuthUser(object): - """A simple object that handles a mercurial username for authentication + """ + A simple object that handles all attributes of user in RhodeCode + + It does lookup based on API key,given user, or user present in session + Then it fills all required information for such user. It also checks if + anonymous access is enabled and if so, it returns default user as logged + in """ - def __init__(self): + def __init__(self, user_id=None, api_key=None): + + self.user_id = user_id + self.api_key = api_key + self.username = 'None' self.name = '' self.lastname = '' self.email = '' - self.user_id = None self.is_authenticated = False - self.is_admin = False + self.admin = False self.permissions = {} + self.propagate_data() + + + def propagate_data(self): + user_model = UserModel() + if self.api_key: + #try go get user by api key + log.debug('Auth User lookup by API KEY %s', self.api_key) + user_model.fill_data(self, api_key=self.api_key) + else: + log.debug('Auth User lookup by USER ID %s', self.user_id) + self.anonymous_user = user_model.get_by_username('default', cache=True) + + if self.user_id is not None and self.user_id != self.anonymous_user.user_id: + user_model.fill_data(self, user_id=self.user_id) + else: + if self.anonymous_user.active is True: + user_model.fill_data(self, user_id=self.anonymous_user.user_id) + #then we set this user is logged in + self.is_authenticated = True + else: + self.is_authenticated = False + + log.debug('Auth User is now %s', self) + user_model.fill_perms(self) + + @property + def is_admin(self): + return self.admin def __repr__(self): - return "" % (self.user_id, self.username) + return "" % (self.user_id, self.username, + self.is_authenticated) + + def set_authenticated(self, authenticated=True): + + if self.user_id != self.anonymous_user.user_id: + self.is_authenticated = authenticated + def set_available_permissions(config): """This function will propagate pylons globals with all available defined @@ -221,144 +258,42 @@ def set_available_permissions(config): config['available_permissions'] = [x.permission_name for x in all_perms] -def fill_perms(user): - """Fills user permission attribute with permissions taken from database - works for permissions given for repositories, and for permissions that - as part of beeing group member - - :param user: user instance to fill his perms - """ - - sa = meta.Session() - user.permissions['repositories'] = {} - user.permissions['global'] = set() - - #=========================================================================== - # fetch default permissions - #=========================================================================== - default_user = UserModel().get_by_username('default', cache=True) - - 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 == default_user).all() - - if user.is_admin: - #======================================================================= - # #admin have all default rights set to admin - #======================================================================= - user.permissions['global'].add('hg.admin') - - for perm in default_perms: - p = 'repository.admin' - user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p - - else: - #======================================================================= - # set default permissions - #======================================================================= - - #default global - default_global_perms = sa.query(UserToPerm)\ - .filter(UserToPerm.user == sa.query(User)\ - .filter(User.username == 'default').one()) - - for perm in default_global_perms: - user.permissions['global'].add(perm.permission.permission_name) - - #default for repositories - 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' - else: - p = perm.Permission.permission_name - - user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p - - #======================================================================= - # overwrite default with user permissions if any - #======================================================================= - user_perms = sa.query(RepoToPerm, Permission, Repository)\ - .join((Repository, RepoToPerm.repository_id == Repository.repo_id))\ - .join((Permission, RepoToPerm.permission_id == Permission.permission_id))\ - .filter(RepoToPerm.user_id == user.user_id).all() - - for perm in user_perms: - if perm.Repository.user_id == user.user_id:#set admin if owner - p = 'repository.admin' - else: - p = perm.Permission.permission_name - user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p - - - #======================================================================= - # check if user is part of groups for this repository and fill in - # (or replace with higher) permissions - #======================================================================= - user_perms_from_users_groups = sa.query(UsersGroupToPerm, Permission, Repository,)\ - .join((Repository, UsersGroupToPerm.repository_id == Repository.repo_id))\ - .join((Permission, UsersGroupToPerm.permission_id == Permission.permission_id))\ - .join((UsersGroupMember, UsersGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\ - .filter(UsersGroupMember.user_id == user.user_id).all() - - for perm in user_perms_from_users_groups: - p = perm.Permission.permission_name - cur_perm = user.permissions['repositories'][perm.UsersGroupToPerm.repository.repo_name] - #overwrite permission only if it's greater than permission given from other sources - if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: - user.permissions['repositories'][perm.UsersGroupToPerm.repository.repo_name] = p - - meta.Session.remove() - return user - -def get_user(session): - """Gets user from session, and wraps permissions into user - - :param session: - """ - user = session.get('rhodecode_user', AuthUser()) - #if the user is not logged in we check for anonymous access - #if user is logged and it's a default user check if we still have anonymous - #access enabled - if user.user_id is None or user.username == 'default': - anonymous_user = UserModel().get_by_username('default', cache=True) - if anonymous_user.active is True: - #then we set this user is logged in - user.is_authenticated = True - user.user_id = anonymous_user.user_id - else: - user.is_authenticated = False - - if user.is_authenticated: - user = UserModel().fill_data(user) - - user = fill_perms(user) - session['rhodecode_user'] = user - session.save() - return user - #=============================================================================== # CHECK DECORATORS #=============================================================================== class LoginRequired(object): - """Must be logged in to execute this function else - redirect to login page""" + """ + Must be logged in to execute this function else + redirect to login page + + :param api_access: if enabled this checks only for valid auth token + and grants access based on valid token + """ + + def __init__(self, api_access=False): + self.api_access = api_access def __call__(self, func): return decorator(self.__wrapper, func) def __wrapper(self, func, *fargs, **fkwargs): - user = session.get('rhodecode_user', AuthUser()) - log.debug('Checking login required for user:%s', user.username) - if user.is_authenticated: + cls = fargs[0] + user = cls.rhodecode_user + + api_access_ok = False + if self.api_access: + log.debug('Checking API KEY access for %s', cls) + if user.api_key == request.GET.get('api_key'): + api_access_ok = True + else: + log.debug("API KEY token not valid") + + log.debug('Checking if %s is authenticated @ %s', user.username, cls) + if user.is_authenticated or api_access_ok: log.debug('user %s is authenticated', user.username) return func(*fargs, **fkwargs) else: - log.warn('user %s not authenticated', user.username) + log.warn('user %s NOT authenticated', user.username) p = '' if request.environ.get('SCRIPT_NAME') != '/': @@ -379,10 +314,12 @@ class NotAnonymous(object): return decorator(self.__wrapper, func) def __wrapper(self, func, *fargs, **fkwargs): - user = session.get('rhodecode_user', AuthUser()) - log.debug('Checking if user is not anonymous') + cls = fargs[0] + self.user = cls.rhodecode_user - anonymous = user.username == 'default' + log.debug('Checking if user is not anonymous @%s', cls) + + anonymous = self.user.username == 'default' if anonymous: p = '' @@ -401,7 +338,7 @@ class NotAnonymous(object): return func(*fargs, **fkwargs) class PermsDecorator(object): - """Base class for decorators""" + """Base class for controller decorators""" def __init__(self, *required_perms): available_perms = config['available_permissions'] @@ -416,22 +353,19 @@ class PermsDecorator(object): def __wrapper(self, func, *fargs, **fkwargs): -# _wrapper.__name__ = func.__name__ -# _wrapper.__dict__.update(func.__dict__) -# _wrapper.__doc__ = func.__doc__ - self.user = session.get('rhodecode_user', AuthUser()) + cls = fargs[0] + self.user = cls.rhodecode_user self.user_perms = self.user.permissions log.debug('checking %s permissions %s for %s %s', - self.__class__.__name__, self.required_perms, func.__name__, + self.__class__.__name__, self.required_perms, cls, self.user) if self.check_permissions(): - log.debug('Permission granted for %s %s', func.__name__, self.user) - + log.debug('Permission granted for %s %s', cls, self.user) return func(*fargs, **fkwargs) else: - log.warning('Permission denied for %s %s', func.__name__, self.user) + log.warning('Permission denied for %s %s', cls, self.user) #redirect with forbidden ret code return abort(403) @@ -516,18 +450,18 @@ class PermsFunction(object): if not user: return False self.user_perms = user.permissions - self.granted_for = user.username + self.granted_for = user log.debug('checking %s %s %s', self.__class__.__name__, self.required_perms, user) if self.check_permissions(): - log.debug('Permission granted for %s @ %s %s', self.granted_for, - check_Location, user) + log.debug('Permission granted %s @ %s', self.granted_for, + check_Location or 'unspecified location') return True else: - log.warning('Permission denied for %s @ %s %s', self.granted_for, - check_Location, user) + log.warning('Permission denied for %s @ %s', self.granted_for, + check_Location or 'unspecified location') return False def check_permissions(self): @@ -595,14 +529,9 @@ class HasPermissionAnyMiddleware(object) self.required_perms = set(perms) def __call__(self, user, repo_name): - usr = AuthUser() - usr.user_id = user.user_id - usr.username = user.username - usr.is_admin = user.admin - + usr = AuthUser(user.user_id) try: - self.user_perms = set([fill_perms(usr)\ - .permissions['repositories'][repo_name]]) + self.user_perms = set([usr.permissions['repositories'][repo_name]]) except: self.user_perms = set() self.granted_for = '' diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -6,7 +6,7 @@ from pylons import config, tmpl_context from pylons.controllers import WSGIController from pylons.templating import render_mako as render from rhodecode import __version__ -from rhodecode.lib import auth +from rhodecode.lib.auth import AuthUser from rhodecode.lib.utils import get_repo_slug from rhodecode.model import meta from rhodecode.model.scm import ScmModel @@ -34,7 +34,14 @@ class BaseController(WSGIController): # available in environ['pylons.routes_dict'] try: #putting this here makes sure that we update permissions every time - self.rhodecode_user = c.rhodecode_user = auth.get_user(session) + api_key = request.GET.get('api_key') + user_id = getattr(session.get('rhodecode_user'), 'user_id', None) + self.rhodecode_user = c.rhodecode_user = AuthUser(user_id, api_key) + self.rhodecode_user.set_authenticated( + getattr(session.get('rhodecode_user'), + 'is_authenticated', False)) + session['rhodecode_user'] = self.rhodecode_user + session.save() return WSGIController.__call__(self, environ, start_response) finally: meta.Session.remove() diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -32,8 +32,8 @@ 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.model.db import User, RepoToPerm, Repository, Permission, \ + UserToPerm, UsersGroupToPerm, UsersGroupMember from rhodecode.lib.exceptions import DefaultUserException, UserOwnsReposException from sqlalchemy.exc import DatabaseError @@ -41,6 +41,11 @@ from rhodecode.lib import generate_api_k log = logging.getLogger(__name__) +PERM_WEIGHTS = {'repository.none':0, + 'repository.read':1, + 'repository.write':3, + 'repository.admin':3} + class UserModel(BaseModel): def get(self, user_id, cache=False): @@ -63,6 +68,16 @@ class UserModel(BaseModel): "get_user_%s" % username)) return user.scalar() + + def get_by_api_key(self, api_key, cache=False): + + user = self.sa.query(User)\ + .filter(User.api_key == api_key) + if cache: + user = user.options(FromCache("sql_cache_short", + "get_user_%s" % api_key)) + return user.scalar() + def create(self, form_data): try: new_user = User() @@ -204,27 +219,125 @@ class UserModel(BaseModel): run_task(tasks.reset_user_password, data['email']) - def fill_data(self, user): + def fill_data(self, auth_user, user_id=None, api_key=None): """ - Fills user data with those from database and log out user if not + Fetches auth_user by user_id,or api_key if present. + Fills auth_user attributes with those taken from database. + Additionally set's is_authenitated if lookup fails present in database - :param user: + + :param auth_user: instance of user to set attributes + :param user_id: user id to fetch by + :param api_key: api key to fetch by """ + if not user_id and not not api_key: + raise Exception('You need to pass user_id or api_key') - if not hasattr(user, 'user_id') or user.user_id is None: - raise Exception('passed in user has to have the user_id attribute') + try: + if api_key: + dbuser = self.get_by_api_key(api_key) + else: + dbuser = self.get(user_id) + + log.debug('filling %s data', dbuser) + for k, v in dbuser.get_dict().items(): + setattr(auth_user, k, v) + + except: + log.error(traceback.format_exc()) + auth_user.is_authenticated = False + + return auth_user - log.debug('filling auth user data') - try: - dbuser = self.get(user.user_id) - user.username = dbuser.username - user.is_admin = dbuser.admin - user.name = dbuser.name - user.lastname = dbuser.lastname - user.email = dbuser.email - except: - log.error(traceback.format_exc()) - user.is_authenticated = False + def fill_perms(self, user): + """Fills user permission attribute with permissions taken from database + works for permissions given for repositories, and for permissions that + as part of beeing group member + + :param user: user instance to fill his perms + """ + + user.permissions['repositories'] = {} + user.permissions['global'] = set() + + #=========================================================================== + # fetch default permissions + #=========================================================================== + default_user = self.get_by_username('default', cache=True) + + default_perms = self.sa.query(RepoToPerm, Repository, Permission)\ + .join((Repository, RepoToPerm.repository_id == Repository.repo_id))\ + .join((Permission, RepoToPerm.permission_id == Permission.permission_id))\ + .filter(RepoToPerm.user == default_user).all() + + if user.is_admin: + #======================================================================= + # #admin have all default rights set to admin + #======================================================================= + user.permissions['global'].add('hg.admin') + + for perm in default_perms: + p = 'repository.admin' + user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p + + else: + #======================================================================= + # set default permissions + #======================================================================= + + #default global + default_global_perms = self.sa.query(UserToPerm)\ + .filter(UserToPerm.user == self.sa.query(User)\ + .filter(User.username == 'default').one()) + + for perm in default_global_perms: + user.permissions['global'].add(perm.permission.permission_name) + + #default for repositories + for perm in default_perms: + if perm.Repository.private and not perm.Repository.user_id == user.user_id: + #diself.sable defaults for private repos, + p = 'repository.none' + elif perm.Repository.user_id == user.user_id: + #set admin if owner + p = 'repository.admin' + else: + p = perm.Permission.permission_name + + user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p + + #======================================================================= + # overwrite default with user permissions if any + #======================================================================= + user_perms = self.sa.query(RepoToPerm, Permission, Repository)\ + .join((Repository, RepoToPerm.repository_id == Repository.repo_id))\ + .join((Permission, RepoToPerm.permission_id == Permission.permission_id))\ + .filter(RepoToPerm.user_id == user.user_id).all() + + for perm in user_perms: + if perm.Repository.user_id == user.user_id:#set admin if owner + p = 'repository.admin' + else: + p = perm.Permission.permission_name + user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p + + + #======================================================================= + # check if user is part of groups for this repository and fill in + # (or replace with higher) permissions + #======================================================================= + user_perms_from_users_groups = self.sa.query(UsersGroupToPerm, Permission, Repository,)\ + .join((Repository, UsersGroupToPerm.repository_id == Repository.repo_id))\ + .join((Permission, UsersGroupToPerm.permission_id == Permission.permission_id))\ + .join((UsersGroupMember, UsersGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\ + .filter(UsersGroupMember.user_id == user.user_id).all() + + for perm in user_perms_from_users_groups: + p = perm.Permission.permission_name + cur_perm = user.permissions['repositories'][perm.UsersGroupToPerm.repository.repo_name] + #overwrite permission only if it's greater than permission given from other sources + if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: + user.permissions['repositories'][perm.UsersGroupToPerm.repository.repo_name] = p return user