base.py
345 lines
| 13.1 KiB
| text/x-python
|
PythonLexer
r547 | """The base Controller API | |||
Provides the BaseController class for subclassing. | ||||
""" | ||||
r1373 | import logging | |||
r1601 | import time | |||
r1813 | import traceback | |||
r1761 | ||||
from paste.auth.basic import AuthBasicAuthenticator | ||||
r2132 | from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden | |||
r2912 | from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION | |||
r1761 | ||||
r1373 | from pylons import config, tmpl_context as c, request, session, url | |||
r547 | from pylons.controllers import WSGIController | |||
r1373 | from pylons.controllers.util import redirect | |||
r547 | from pylons.templating import render_mako as render | |||
r1304 | ||||
r1718 | from rhodecode import __version__, BACKENDS | |||
r2726 | from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\ | |||
r3087 | safe_str, safe_int | |||
r1761 | from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\ | |||
r3146 | HasPermissionAnyMiddleware, CookieStoreWrapper | |||
r3693 | from rhodecode.lib.utils import get_repo_slug | |||
r547 | from rhodecode.model import meta | |||
r1718 | ||||
r2955 | from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting | |||
r1702 | from rhodecode.model.notification import NotificationModel | |||
r1718 | from rhodecode.model.scm import ScmModel | |||
r2726 | from rhodecode.model.meta import Session | |||
r665 | ||||
r1373 | log = logging.getLogger(__name__) | |||
r1307 | ||||
r1813 | ||||
r3669 | def _filter_proxy(ip): | |||
""" | ||||
HEADERS can have mutliple ips inside the left-most being the original | ||||
client, and each successive proxy that passed the request adding the IP | ||||
address where it received the request from. | ||||
:param ip: | ||||
""" | ||||
if ',' in ip: | ||||
_ips = ip.split(',') | ||||
_first_ip = _ips[0].strip() | ||||
log.debug('Got multiple IPs %s, using %s' % (','.join(_ips), _first_ip)) | ||||
return _first_ip | ||||
return ip | ||||
r2374 | def _get_ip_addr(environ): | |||
proxy_key = 'HTTP_X_REAL_IP' | ||||
proxy_key2 = 'HTTP_X_FORWARDED_FOR' | ||||
def_key = 'REMOTE_ADDR' | ||||
r3153 | ip = environ.get(proxy_key) | |||
r2486 | if ip: | |||
r3669 | return _filter_proxy(ip) | |||
r2486 | ||||
r3153 | ip = environ.get(proxy_key2) | |||
r2486 | if ip: | |||
r3669 | return _filter_proxy(ip) | |||
r2486 | ||||
ip = environ.get(def_key, '0.0.0.0') | ||||
r3669 | return _filter_proxy(ip) | |||
r2374 | ||||
r2490 | def _get_access_path(environ): | |||
path = environ.get('PATH_INFO') | ||||
org_req = environ.get('pylons.original_request') | ||||
if org_req: | ||||
path = org_req.environ.get('PATH_INFO') | ||||
return path | ||||
r2132 | class BasicAuth(AuthBasicAuthenticator): | |||
def __init__(self, realm, authfunc, auth_http_code=None): | ||||
self.realm = realm | ||||
self.authfunc = authfunc | ||||
self._rc_auth_http_code = auth_http_code | ||||
def build_authentication(self): | ||||
head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) | ||||
if self._rc_auth_http_code and self._rc_auth_http_code == '403': | ||||
# return 403 if alternative http return code is specified in | ||||
# RhodeCode config | ||||
return HTTPForbidden(headers=head) | ||||
return HTTPUnauthorized(headers=head) | ||||
r2912 | def authenticate(self, environ): | |||
authorization = AUTHORIZATION(environ) | ||||
if not authorization: | ||||
return self.build_authentication() | ||||
(authmeth, auth) = authorization.split(' ', 1) | ||||
if 'basic' != authmeth.lower(): | ||||
return self.build_authentication() | ||||
auth = auth.strip().decode('base64') | ||||
_parts = auth.split(':', 1) | ||||
if len(_parts) == 2: | ||||
username, password = _parts | ||||
if self.authfunc(environ, username, password): | ||||
return username | ||||
return self.build_authentication() | ||||
__call__ = authenticate | ||||
r2132 | ||||
r1761 | class BaseVCSController(object): | |||
r1813 | ||||
r1761 | def __init__(self, application, config): | |||
self.application = application | ||||
self.config = config | ||||
# base path of repo locations | ||||
self.basepath = self.config['base_path'] | ||||
#authenticate this mercurial request using authfunc | ||||
r2132 | self.authenticate = BasicAuth('', authfunc, | |||
config.get('auth_ret_code')) | ||||
r3125 | self.ip_addr = '0.0.0.0' | |||
r1813 | ||||
r1978 | def _handle_request(self, environ, start_response): | |||
raise NotImplementedError() | ||||
r1813 | def _get_by_id(self, repo_name): | |||
""" | ||||
Get's a special pattern _<ID> from clone url and tries to replace it | ||||
with a repository_name for support of _<ID> non changable urls | ||||
:param repo_name: | ||||
""" | ||||
try: | ||||
data = repo_name.split('/') | ||||
if len(data) >= 2: | ||||
by_id = data[1].split('_') | ||||
if len(by_id) == 2 and by_id[1].isdigit(): | ||||
_repo_name = Repository.get(by_id[1]).repo_name | ||||
data[1] = _repo_name | ||||
r3631 | except Exception: | |||
r1813 | log.debug('Failed to extract repo_name from id %s' % ( | |||
traceback.format_exc() | ||||
) | ||||
) | ||||
return '/'.join(data) | ||||
r1761 | def _invalidate_cache(self, repo_name): | |||
""" | ||||
Set's cache for this repository for invalidation on next access | ||||
r1813 | ||||
r1761 | :param repo_name: full repo name, also a cache key | |||
""" | ||||
r3693 | ScmModel().mark_for_invalidation(repo_name) | |||
r1813 | ||||
r3125 | def _check_permission(self, action, user, repo_name, ip_addr=None): | |||
r1761 | """ | |||
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 | ||||
""" | ||||
r3125 | #check IP | |||
r3146 | authuser = AuthUser(user_id=user.user_id, ip_addr=ip_addr) | |||
if not authuser.ip_allowed: | ||||
r3125 | return False | |||
else: | ||||
log.info('Access for IP:%s allowed' % (ip_addr)) | ||||
r1761 | 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 | ||||
r1813 | return True | |||
r2184 | def _get_ip_addr(self, environ): | |||
r2374 | return _get_ip_addr(environ) | |||
r2184 | ||||
r2668 | def _check_ssl(self, environ, start_response): | |||
""" | ||||
Checks the SSL check flag and returns False if SSL is not present | ||||
and required True otherwise | ||||
""" | ||||
org_proto = environ['wsgi._org_proto'] | ||||
#check if we have SSL required ! if not it's a bad request ! | ||||
r2708 | require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl').ui_value) | |||
r2668 | if require_ssl and org_proto == 'http': | |||
log.debug('proto is %s and SSL is required BAD REQUEST !' | ||||
% org_proto) | ||||
return False | ||||
r2674 | return True | |||
r2668 | ||||
r2726 | def _check_locking_state(self, environ, action, repo, user_id): | |||
""" | ||||
Checks locking on this repository, if locking is enabled and lock is | ||||
present returns a tuple of make_lock, locked, locked_by. | ||||
make_lock can have 3 states None (do nothing) True, make lock | ||||
False release lock, This value is later propagated to hooks, which | ||||
do the locking. Think about this as signals passed to hooks what to do. | ||||
""" | ||||
r2752 | locked = False # defines that locked error should be thrown to user | |||
r2726 | make_lock = None | |||
repo = Repository.get_by_repo_name(repo) | ||||
user = User.get(user_id) | ||||
# this is kind of hacky, but due to how mercurial handles client-server | ||||
# server see all operation on changeset; bookmarks, phases and | ||||
# obsolescence marker in different transaction, we don't want to check | ||||
# locking on those | ||||
obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',] | ||||
locked_by = repo.locked | ||||
if repo and repo.enable_locking and not obsolete_call: | ||||
if action == 'push': | ||||
#check if it's already locked !, if it is compare users | ||||
user_id, _date = repo.locked | ||||
if user.user_id == user_id: | ||||
r2752 | log.debug('Got push from user %s, now unlocking' % (user)) | |||
r2726 | # unlock if we have push from user who locked | |||
make_lock = False | ||||
else: | ||||
# we're not the same user who locked, ban with 423 ! | ||||
locked = True | ||||
if action == 'pull': | ||||
if repo.locked[0] and repo.locked[1]: | ||||
locked = True | ||||
else: | ||||
log.debug('Setting lock on repo %s by %s' % (repo, user)) | ||||
make_lock = True | ||||
else: | ||||
log.debug('Repository %s do not have locking enabled' % (repo)) | ||||
r2752 | log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s' | |||
% (make_lock, locked, locked_by)) | ||||
r2726 | return make_lock, locked, locked_by | |||
r1761 | def __call__(self, environ, start_response): | |||
start = time.time() | ||||
try: | ||||
return self._handle_request(environ, start_response) | ||||
finally: | ||||
r1763 | log = logging.getLogger('rhodecode.' + self.__class__.__name__) | |||
r1761 | log.debug('Request time: %.3fs' % (time.time() - start)) | |||
meta.Session.remove() | ||||
r547 | class BaseController(WSGIController): | |||
r659 | ||||
r547 | def __before__(self): | |||
r3125 | """ | |||
__before__ is called before controller methods and after __call__ | ||||
""" | ||||
r548 | c.rhodecode_version = __version__ | |||
r2016 | c.rhodecode_instanceid = config.get('instance_id') | |||
r890 | c.rhodecode_name = config.get('rhodecode_title') | |||
r1629 | c.use_gravatar = str2bool(config.get('use_gravatar')) | |||
r891 | c.ga_code = config.get('rhodecode_ga_code') | |||
r2674 | # Visual options | |||
c.visual = AttributeDict({}) | ||||
r2955 | rc_config = RhodeCodeSetting.get_app_settings() | |||
r3904 | ## DB stored | |||
r2955 | c.visual.show_public_icon = str2bool(rc_config.get('rhodecode_show_public_icon')) | |||
c.visual.show_private_icon = str2bool(rc_config.get('rhodecode_show_private_icon')) | ||||
c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags')) | ||||
r3904 | c.visual.dashboard_items = safe_int(rc_config.get('rhodecode_dashboard_items', 100)) | |||
r3308 | c.visual.repository_fields = str2bool(rc_config.get('rhodecode_repository_fields')) | |||
r3904 | ## INI stored | |||
self.cut_off_limit = int(config.get('cut_off_limit')) | ||||
r3580 | c.repo_name = get_repo_slug(request) # can be empty | |||
r659 | c.backends = BACKENDS.keys() | |||
r1702 | c.unread_notifications = NotificationModel()\ | |||
.get_unread_cnt_for_user(c.rhodecode_user.user_id) | ||||
r1749 | self.sa = meta.Session | |||
r1045 | self.scm_model = ScmModel(self.sa) | |||
r1366 | ||||
r547 | def __call__(self, environ, start_response): | |||
"""Invoke the Controller""" | ||||
# WSGIController.__call__ dispatches to the Controller method | ||||
# the request is routed to. This routing information is | ||||
# available in environ['pylons.routes_dict'] | ||||
try: | ||||
r2374 | self.ip_addr = _get_ip_addr(environ) | |||
r1628 | # make sure that we update permissions each time we call controller | |||
r1117 | api_key = request.GET.get('api_key') | |||
r2030 | cookie_store = CookieStoreWrapper(session.get('rhodecode_user')) | |||
r1718 | user_id = cookie_store.get('user_id', None) | |||
Liad Shani
|
r1630 | username = get_container_username(environ, config) | ||
r3125 | auth_user = AuthUser(user_id, api_key, username, self.ip_addr) | |||
r1728 | request.user = auth_user | |||
r1628 | self.rhodecode_user = c.rhodecode_user = auth_user | |||
Liad Shani
|
r1618 | if not self.rhodecode_user.is_authenticated and \ | ||
self.rhodecode_user.user_id is not None: | ||||
r2030 | self.rhodecode_user.set_authenticated( | |||
cookie_store.get('is_authenticated') | ||||
) | ||||
r2486 | log.info('IP: %s User: %s accessed %s' % ( | |||
r2490 | self.ip_addr, auth_user, safe_unicode(_get_access_path(environ))) | |||
r2027 | ) | |||
r547 | return WSGIController.__call__(self, environ, start_response) | |||
finally: | ||||
meta.Session.remove() | ||||
r1045 | ||||
class BaseRepoController(BaseController): | ||||
""" | ||||
r1628 | Base class for controllers responsible for loading all needed data for | |||
repository loaded items are | ||||
r1203 | ||||
r1628 | c.rhodecode_repo: instance of scm repository | |||
c.rhodecode_db_repo: instance of db | ||||
c.repository_followers: number of followers | ||||
c.repository_forks: number of forks | ||||
Leonardo
|
r3527 | c.repository_following: weather the current user is following the current repo | ||
r1045 | """ | |||
def __before__(self): | ||||
super(BaseRepoController, self).__before__() | ||||
if c.repo_name: | ||||
r2440 | dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name) | |||
r1373 | c.rhodecode_repo = c.rhodecode_db_repo.scm_instance | |||
r2937 | # update last change according to VCS data | |||
r3147 | dbr.update_changeset_cache(dbr.get_changeset()) | |||
r1373 | if c.rhodecode_repo is None: | |||
log.error('%s this repository is present in database but it ' | ||||
'cannot be created as an scm instance', c.repo_name) | ||||
r1282 | ||||
r1373 | redirect(url('home')) | |||
r1304 | ||||
r2440 | # some globals counter for menu | |||
c.repository_followers = self.scm_model.get_followers(dbr) | ||||
c.repository_forks = self.scm_model.get_forks(dbr) | ||||
r2478 | c.repository_pull_requests = self.scm_model.get_pull_requests(dbr) | |||
Leonardo
|
r3527 | c.repository_following = self.scm_model.is_following_repo(c.repo_name, | ||
self.rhodecode_user.user_id) | ||||