diff --git a/rhodecode/lib/middleware/simplegit.py b/rhodecode/lib/middleware/simplegit.py --- a/rhodecode/lib/middleware/simplegit.py +++ b/rhodecode/lib/middleware/simplegit.py @@ -1,8 +1,16 @@ -#!/usr/bin/env python -# encoding: utf-8 -# middleware to handle git api calls -# Copyright (C) 2009-2010 Marcin Kuzminski -# +# -*- coding: utf-8 -*- +""" + rhodecode.lib.middleware.simplegit + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + SimpleGit middleware for handling git protocol request (push/clone etc.) + It's implemented with basic auth function + + :created_on: Apr 28, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :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, either version 3 of the License, or @@ -15,34 +23,33 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -""" -Created on 2010-04-28 -@author: marcink -SimpleGit middleware for handling git protocol request (push/clone etc.) -It's implemented with basic auth function -""" +import os +import logging +import traceback from dulwich import server as dulserver + class SimpleGitUploadPackHandler(dulserver.UploadPackHandler): def handle(self): write = lambda x: self.proto.write_sideband(1, x) - graph_walker = dulserver.ProtocolGraphWalker(self, self.repo.object_store, - self.repo.get_peeled) + graph_walker = dulserver.ProtocolGraphWalker(self, + self.repo.object_store, + self.repo.get_peeled) objects_iter = self.repo.fetch_objects( graph_walker.determine_wants, graph_walker, self.progress, get_tagged=self.get_tagged) # Do they want any objects? - if len(objects_iter) == 0: + if objects_iter is None or len(objects_iter) == 0: return self.progress("counting objects: %d, done.\n" % len(objects_iter)) - dulserver.write_pack_data(dulserver.ProtocolFile(None, write), objects_iter, - len(objects_iter)) + dulserver.write_pack_objects(dulserver.ProtocolFile(None, write), + objects_iter, len(objects_iter)) messages = [] messages.append('thank you for using rhodecode') @@ -58,23 +65,24 @@ dulserver.DEFAULT_HANDLERS = { from dulwich.repo import Repo from dulwich.web import HTTPGitApplication + from paste.auth.basic import AuthBasicAuthenticator from paste.httpheaders import REMOTE_USER, AUTH_TYPE + +from rhodecode.lib import safe_str from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware -from rhodecode.lib.utils import invalidate_cache, check_repo_fast -from rhodecode.model.user import UserModel +from rhodecode.lib.utils import invalidate_cache, is_valid_repo +from rhodecode.model.db import User + from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError -import logging -import os -import traceback log = logging.getLogger(__name__) + def is_git(environ): - """ - Returns True if request's target is git server. ``HTTP_USER_AGENT`` would - then have git client version given. - + """Returns True if request's target is git server. + ``HTTP_USER_AGENT`` would then have git client version given. + :param environ: """ http_user_agent = environ.get('HTTP_USER_AGENT') @@ -82,17 +90,16 @@ def is_git(environ): return True return False + class SimpleGit(object): def __init__(self, application, config): self.application = application self.config = config - #authenticate this git request using + # base path of repo locations + self.basepath = self.config['base_path'] + #authenticate this mercurial request using authfunc self.authenticate = AuthBasicAuthenticator('', authfunc) - self.ipaddr = '0.0.0.0' - self.repository = None - self.username = None - self.action = None def __call__(self, environ, start_response): if not is_git(environ): @@ -100,105 +107,166 @@ class SimpleGit(object): proxy_key = 'HTTP_X_REAL_IP' def_key = 'REMOTE_ADDR' - self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0')) + ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0')) + username = None + # skip passing error to error controller + environ['pylons.status_code_redirect'] = True + + #====================================================================== + # EXTRACT REPOSITORY NAME FROM ENV + #====================================================================== + try: + repo_name = self.__get_repository(environ) + log.debug('Extracted repo name is %s' % repo_name) + except: + return HTTPInternalServerError()(environ, start_response) + + #====================================================================== + # GET ACTION PULL or PUSH + #====================================================================== + action = self.__get_action(environ) + + #====================================================================== + # CHECK ANONYMOUS PERMISSION + #====================================================================== + if action in ['pull', 'push']: + anonymous_user = self.__get_user('default') + username = anonymous_user.username + anonymous_perm = self.__check_permission(action, + anonymous_user, + repo_name) + + if anonymous_perm is not True or anonymous_user.active is False: + if anonymous_perm is not True: + log.debug('Not enough credentials to access this ' + 'repository as anonymous user') + if anonymous_user.active is False: + log.debug('Anonymous access is disabled, running ' + 'authentication') + #============================================================== + # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE + # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS + #============================================================== + + if not REMOTE_USER(environ): + self.authenticate.realm = \ + safe_str(self.config['rhodecode_realm']) + result = self.authenticate(environ) + if isinstance(result, str): + AUTH_TYPE.update(environ, 'basic') + REMOTE_USER.update(environ, result) + else: + return result.wsgi_application(environ, start_response) + + #============================================================== + # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM + # BASIC AUTH + #============================================================== + + if action in ['pull', 'push']: + username = REMOTE_USER(environ) + try: + user = self.__get_user(username) + username = user.username + except: + log.error(traceback.format_exc()) + return HTTPInternalServerError()(environ, + start_response) + + #check permissions for this repository + perm = self.__check_permission(action, user, + repo_name) + if perm is not True: + return HTTPForbidden()(environ, start_response) + + extras = {'ip': ipaddr, + 'username': username, + 'action': action, + 'repository': repo_name} #=================================================================== - # AUTHENTICATE THIS GIT REQUEST + # GIT REQUEST HANDLING #=================================================================== - username = REMOTE_USER(environ) - if not username: - self.authenticate.realm = self.config['rhodecode_realm'] - result = self.authenticate(environ) - if isinstance(result, str): - AUTH_TYPE.update(environ, 'basic') - REMOTE_USER.update(environ, result) - else: - return result.wsgi_application(environ, start_response) + + repo_path = safe_str(os.path.join(self.basepath, repo_name)) + log.debug('Repository path is %s' % repo_path) + + # quick check if that dir exists... + if is_valid_repo(repo_name, self.basepath) is False: + return HTTPNotFound()(environ, start_response) + + try: + #invalidate cache on push + if action == 'push': + self.__invalidate_cache(repo_name) + + app = self.__make_app(repo_name, repo_path) + return app(environ, start_response) + except Exception: + log.error(traceback.format_exc()) + return HTTPInternalServerError()(environ, start_response) + + def __make_app(self, repo_name, repo_path): + """ + Make an wsgi application using dulserver + + :param repo_name: name of the repository + :param repo_path: full path to the repository + """ + + _d = {'/' + repo_name: Repo(repo_path)} + backend = dulserver.DictBackend(_d) + gitserve = HTTPGitApplication(backend) - #======================================================================= - # GET REPOSITORY - #======================================================================= + return gitserve + + def __check_permission(self, action, user, repo_name): + """ + Checks permissions using action (push/pull) user and repository + name + + :param action: push or pull action + :param user: user instance + :param repo_name: repository name + """ + if action == 'push': + if not HasPermissionAnyMiddleware('repository.write', + 'repository.admin')(user, + repo_name): + return False + + else: + #any other action need at least read permission + if not HasPermissionAnyMiddleware('repository.read', + 'repository.write', + 'repository.admin')(user, + repo_name): + return False + + return True + + def __get_repository(self, environ): + """ + Get's repository name out of PATH_INFO header + + :param environ: environ where PATH_INFO is stored + """ try: repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:]) if repo_name.endswith('/'): repo_name = repo_name.rstrip('/') - self.repository = repo_name except: log.error(traceback.format_exc()) - return HTTPInternalServerError()(environ, start_response) - - #=================================================================== - # CHECK PERMISSIONS FOR THIS REQUEST - #=================================================================== - self.action = self.__get_action(environ) - if self.action: - username = self.__get_environ_user(environ) - try: - user = self.__get_user(username) - self.username = user.username - except: - log.error(traceback.format_exc()) - return HTTPInternalServerError()(environ, start_response) - - #check permissions for this repository - if self.action == 'push': - if not HasPermissionAnyMiddleware('repository.write', - 'repository.admin')\ - (user, repo_name): - return HTTPForbidden()(environ, start_response) - - else: - #any other action need at least read permission - if not HasPermissionAnyMiddleware('repository.read', - 'repository.write', - 'repository.admin')\ - (user, repo_name): - return HTTPForbidden()(environ, start_response) - - self.extras = {'ip':self.ipaddr, - 'username':self.username, - 'action':self.action, - 'repository':self.repository} - - #=================================================================== - # GIT REQUEST HANDLING - #=================================================================== - self.basepath = self.config['base_path'] - self.repo_path = os.path.join(self.basepath, self.repo_name) - #quick check if that dir exists... - if check_repo_fast(self.repo_name, self.basepath): - return HTTPNotFound()(environ, start_response) - try: - app = self.__make_app() - except: - log.error(traceback.format_exc()) - return HTTPInternalServerError()(environ, start_response) - - #invalidate cache on push - if self.action == 'push': - self.__invalidate_cache(self.repo_name) - messages = [] - messages.append('thank you for using rhodecode') - return app(environ, start_response) - else: - return app(environ, start_response) - - - def __make_app(self): - backend = dulserver.DictBackend({'/' + self.repo_name: Repo(self.repo_path)}) - gitserve = HTTPGitApplication(backend) - - return gitserve - - def __get_environ_user(self, environ): - return environ.get('REMOTE_USER') + raise + repo_name = repo_name.split('/')[0] + return repo_name def __get_user(self, username): - return UserModel().get_by_username(username, cache=True) + return User.by_username(username) def __get_action(self, environ): - """ - Maps git request commands into a pull or push command. + """Maps git request commands into a pull or push command. + :param environ: """ service = environ['QUERY_STRING'].split('=') @@ -208,7 +276,8 @@ class SimpleGit(object): 'git-upload-pack': 'pull', } - return mapping.get(service_cmd, service_cmd if service_cmd else 'other') + return mapping.get(service_cmd, + service_cmd if service_cmd else 'other') else: return 'other' @@ -217,3 +286,4 @@ class SimpleGit(object): invalidate the cache to see the changes right away but only for push requests""" invalidate_cache('get_repo_cached_%s' % repo_name) +