base.py
649 lines
| 26.6 KiB
| text/x-python
|
PythonLexer
Bradley M. Kuhn
|
r4187 | # -*- coding: utf-8 -*- | ||
# 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 | ||||
# (at your option) any later version. | ||||
# | ||||
# 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, see <http://www.gnu.org/licenses/>. | ||||
""" | ||||
kallithea.lib.base | ||||
~~~~~~~~~~~~~~~~~~ | ||||
The base Controller API | ||||
Provides the BaseController class for subclassing. And usage in different | ||||
controllers | ||||
Bradley M. Kuhn
|
r4211 | This file was forked by the Kallithea project in July 2014. | ||
Original author and date, and relevant copyright and licensing information is below: | ||||
Bradley M. Kuhn
|
r4187 | :created_on: Oct 06, 2010 | ||
:author: marcink | ||||
Bradley M. Kuhn
|
r4211 | :copyright: (c) 2013 RhodeCode GmbH, and others. | ||
Bradley M. Kuhn
|
r4208 | :license: GPLv3, see LICENSE.md for more details. | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Mads Kiilerich
|
r7905 | import base64 | ||
Søren Løvborg
|
r5256 | import datetime | ||
Bradley M. Kuhn
|
r4187 | import logging | ||
import traceback | ||||
Thomas De Schampheleire
|
r6409 | import warnings | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7718 | import decorator | ||
import paste.auth.basic | ||||
Mads Kiilerich
|
r4364 | import paste.httpexceptions | ||
import paste.httpheaders | ||||
Mads Kiilerich
|
r7718 | import webob.exc | ||
from tg import TGController, config, render_template, request, response, session | ||||
from tg import tmpl_context as c | ||||
Mads Kiilerich
|
r6508 | from tg.i18n import ugettext as _ | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7718 | from kallithea import BACKENDS, __version__ | ||
Thomas De Schampheleire
|
r6182 | from kallithea.config.routing import url | ||
Mads Kiilerich
|
r7987 | from kallithea.lib import auth_modules, ext_json | ||
Søren Løvborg
|
r5265 | from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware | ||
Mads Kiilerich
|
r7718 | from kallithea.lib.exceptions import UserCreationError | ||
Mads Kiilerich
|
r7630 | from kallithea.lib.utils import get_repo_slug, is_valid_repo | ||
Mads Kiilerich
|
r8078 | from kallithea.lib.utils2 import AttributeDict, ascii_bytes, safe_int, safe_str, set_hook_environment, str2bool | ||
Mads Kiilerich
|
r7718 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError | ||
Bradley M. Kuhn
|
r4187 | from kallithea.model import meta | ||
Mads Kiilerich
|
r7718 | from kallithea.model.db import PullRequest, Repository, Setting, User | ||
from kallithea.model.scm import ScmModel | ||||
Bradley M. Kuhn
|
r4187 | |||
log = logging.getLogger(__name__) | ||||
Alessandro Molina
|
r6522 | def render(template_path): | ||
return render_template({'url': url}, 'mako', template_path) | ||||
Bradley M. Kuhn
|
r4187 | def _filter_proxy(ip): | ||
""" | ||||
HEADERS can have multiple 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() | ||||
Mads Kiilerich
|
r5375 | log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip) | ||
Bradley M. Kuhn
|
r4187 | return _first_ip | ||
return ip | ||||
def _get_ip_addr(environ): | ||||
proxy_key = 'HTTP_X_REAL_IP' | ||||
proxy_key2 = 'HTTP_X_FORWARDED_FOR' | ||||
def_key = 'REMOTE_ADDR' | ||||
ip = environ.get(proxy_key) | ||||
if ip: | ||||
return _filter_proxy(ip) | ||||
ip = environ.get(proxy_key2) | ||||
if ip: | ||||
return _filter_proxy(ip) | ||||
ip = environ.get(def_key, '0.0.0.0') | ||||
return _filter_proxy(ip) | ||||
Mads Kiilerich
|
r7948 | def get_path_info(environ): | ||
Mads Kiilerich
|
r8082 | """Return PATH_INFO from environ ... using tg.original_request if available. | ||
In Python 3 WSGI, PATH_INFO is a unicode str, but kind of contains encoded | ||||
bytes. The code points are guaranteed to only use the lower 8 bit bits, and | ||||
encoding the string with the 1:1 encoding latin1 will give the | ||||
corresponding byte string ... which then can be decoded to proper unicode. | ||||
Mads Kiilerich
|
r7948 | """ | ||
Alessandro Molina
|
r6522 | org_req = environ.get('tg.original_request') | ||
Mads Kiilerich
|
r7629 | if org_req is not None: | ||
environ = org_req.environ | ||||
Mads Kiilerich
|
r8082 | return safe_str(environ['PATH_INFO'].encode('latin1')) | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7603 | def log_in_user(user, remember, is_external_auth, ip_addr): | ||
Søren Løvborg
|
r5256 | """ | ||
Log a `User` in and update session and cookies. If `remember` is True, | ||||
the session cookie is set to expire in a year; otherwise, it expires at | ||||
the end of the browser session. | ||||
Søren Løvborg
|
r5264 | |||
Returns populated `AuthUser` object. | ||||
Søren Løvborg
|
r5256 | """ | ||
Mads Kiilerich
|
r7602 | # It should not be possible to explicitly log in as the default user. | ||
assert not user.is_default_user, user | ||||
Mads Kiilerich
|
r7603 | auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr) | ||
Mads Kiilerich
|
r7602 | if auth_user is None: | ||
return None | ||||
Søren Løvborg
|
r5256 | user.update_lastlogin() | ||
meta.Session().commit() | ||||
# Start new session to prevent session fixation attacks. | ||||
session.invalidate() | ||||
Søren Løvborg
|
r5265 | session['authuser'] = cookie = auth_user.to_cookie() | ||
Søren Løvborg
|
r5256 | |||
Mads Kiilerich
|
r5400 | # If they want to be remembered, update the cookie. | ||
# NOTE: Assumes that beaker defaults to browser session cookie. | ||||
Søren Løvborg
|
r5256 | if remember: | ||
t = datetime.datetime.now() + datetime.timedelta(days=365) | ||||
session._set_cookie_expires(t) | ||||
session.save() | ||||
log.info('user %s is now authenticated and stored in ' | ||||
Søren Løvborg
|
r5265 | 'session, session attrs %s', user.username, cookie) | ||
Søren Løvborg
|
r5256 | |||
# dumps session attrs back to cookie | ||||
session._update_cookie_out() | ||||
Søren Løvborg
|
r5264 | return auth_user | ||
Søren Løvborg
|
r5256 | |||
Mads Kiilerich
|
r4364 | class BasicAuth(paste.auth.basic.AuthBasicAuthenticator): | ||
Bradley M. Kuhn
|
r4187 | |||
def __init__(self, realm, authfunc, auth_http_code=None): | ||||
self.realm = realm | ||||
self.authfunc = authfunc | ||||
self._rc_auth_http_code = auth_http_code | ||||
domruf
|
r6851 | def build_authentication(self, environ): | ||
Mads Kiilerich
|
r4364 | head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) | ||
domruf
|
r6851 | # Consume the whole body before sending a response | ||
try: | ||||
request_body_size = int(environ.get('CONTENT_LENGTH', 0)) | ||||
except (ValueError): | ||||
request_body_size = 0 | ||||
environ['wsgi.input'].read(request_body_size) | ||||
Bradley M. Kuhn
|
r4187 | if self._rc_auth_http_code and self._rc_auth_http_code == '403': | ||
# return 403 if alternative http return code is specified in | ||||
Bradley M. Kuhn
|
r4212 | # Kallithea config | ||
Mads Kiilerich
|
r4364 | return paste.httpexceptions.HTTPForbidden(headers=head) | ||
return paste.httpexceptions.HTTPUnauthorized(headers=head) | ||||
Bradley M. Kuhn
|
r4187 | |||
def authenticate(self, environ): | ||||
Mads Kiilerich
|
r4364 | authorization = paste.httpheaders.AUTHORIZATION(environ) | ||
Bradley M. Kuhn
|
r4187 | if not authorization: | ||
domruf
|
r6851 | return self.build_authentication(environ) | ||
Bradley M. Kuhn
|
r4187 | (authmeth, auth) = authorization.split(' ', 1) | ||
if 'basic' != authmeth.lower(): | ||||
domruf
|
r6851 | return self.build_authentication(environ) | ||
Mads Kiilerich
|
r8079 | auth = safe_str(base64.b64decode(auth.strip())) | ||
Bradley M. Kuhn
|
r4187 | _parts = auth.split(':', 1) | ||
if len(_parts) == 2: | ||||
username, password = _parts | ||||
Mads Kiilerich
|
r5337 | if self.authfunc(username, password, environ) is not None: | ||
Bradley M. Kuhn
|
r4187 | return username | ||
domruf
|
r6851 | return self.build_authentication(environ) | ||
Bradley M. Kuhn
|
r4187 | |||
__call__ = authenticate | ||||
class BaseVCSController(object): | ||||
Søren Løvborg
|
r6452 | """Base controller for handling Mercurial/Git protocol requests | ||
(coming from a VCS client, and not a browser). | ||||
""" | ||||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7627 | scm_alias = None # 'hg' / 'git' | ||
Bradley M. Kuhn
|
r4187 | def __init__(self, application, config): | ||
self.application = application | ||||
self.config = config | ||||
# base path of repo locations | ||||
self.basepath = self.config['base_path'] | ||||
Mads Kiilerich
|
r5337 | # authenticate this VCS request using the authentication modules | ||
Bradley M. Kuhn
|
r4187 | self.authenticate = BasicAuth('', auth_modules.authenticate, | ||
config.get('auth_ret_code')) | ||||
Mads Kiilerich
|
r7624 | @classmethod | ||
def parse_request(cls, environ): | ||||
"""If request is parsed as a request for this VCS, return a namespace with the parsed request. | ||||
If the request is unknown, return None. | ||||
""" | ||||
raise NotImplementedError() | ||||
Mads Kiilerich
|
r7629 | def _authorize(self, environ, action, repo_name, ip_addr): | ||
Søren Løvborg
|
r6452 | """Authenticate and authorize user. | ||
Since we're dealing with a VCS client and not a browser, we only | ||||
support HTTP basic authentication, either directly via raw header | ||||
inspection, or by using container authentication to delegate the | ||||
authentication to the web server. | ||||
Returns (user, None) on successful authentication and authorization. | ||||
Returns (None, wsgi_app) to send the wsgi_app response to the client. | ||||
""" | ||||
Mads Kiilerich
|
r7602 | # Use anonymous access if allowed for action on repo. | ||
Søren Løvborg
|
r6453 | default_user = User.get_default_user(cache=True) | ||
Mads Kiilerich
|
r7603 | default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) | ||
Mads Kiilerich
|
r7602 | if default_authuser is None: | ||
log.debug('No anonymous access at all') # move on to proper user auth | ||||
Søren Løvborg
|
r6452 | else: | ||
Mads Kiilerich
|
r7603 | if self._check_permission(action, default_authuser, repo_name): | ||
Mads Kiilerich
|
r7602 | return default_authuser, None | ||
log.debug('Not authorized to access this repository as anonymous user') | ||||
Søren Løvborg
|
r6452 | |||
Søren Løvborg
|
r6453 | username = None | ||
#============================================================== | ||||
# DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE | ||||
# NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS | ||||
#============================================================== | ||||
Søren Løvborg
|
r6452 | |||
Søren Løvborg
|
r6453 | # try to auth based on environ, container auth methods | ||
log.debug('Running PRE-AUTH for container based authentication') | ||||
pre_auth = auth_modules.authenticate('', '', environ) | ||||
if pre_auth is not None and pre_auth.get('username'): | ||||
username = pre_auth['username'] | ||||
log.debug('PRE-AUTH got %s as username', username) | ||||
Søren Løvborg
|
r6452 | |||
Søren Løvborg
|
r6453 | # If not authenticated by the container, running basic auth | ||
if not username: | ||||
Mads Kiilerich
|
r8076 | self.authenticate.realm = self.config['realm'] | ||
Søren Løvborg
|
r6453 | result = self.authenticate(environ) | ||
if isinstance(result, str): | ||||
paste.httpheaders.AUTH_TYPE.update(environ, 'basic') | ||||
paste.httpheaders.REMOTE_USER.update(environ, result) | ||||
username = result | ||||
else: | ||||
return None, result.wsgi_application | ||||
Søren Løvborg
|
r6452 | |||
Søren Løvborg
|
r6453 | #============================================================== | ||
# CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME | ||||
#============================================================== | ||||
try: | ||||
user = User.get_by_username_or_email(username) | ||||
except Exception: | ||||
log.error(traceback.format_exc()) | ||||
return None, webob.exc.HTTPInternalServerError() | ||||
Søren Løvborg
|
r6452 | |||
Mads Kiilerich
|
r7603 | authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr) | ||
Mads Kiilerich
|
r7602 | if authuser is None: | ||
return None, webob.exc.HTTPForbidden() | ||||
Mads Kiilerich
|
r7603 | if not self._check_permission(action, authuser, repo_name): | ||
Søren Løvborg
|
r6453 | return None, webob.exc.HTTPForbidden() | ||
Søren Løvborg
|
r6452 | |||
return user, None | ||||
Bradley M. Kuhn
|
r4187 | |||
def _handle_request(self, environ, start_response): | ||||
raise NotImplementedError() | ||||
Mads Kiilerich
|
r7603 | def _check_permission(self, action, authuser, repo_name): | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Checks permissions using action (push/pull) user and repository | ||||
name | ||||
Mads Kiilerich
|
r7317 | :param action: 'push' or 'pull' action | ||
Søren Løvborg
|
r5251 | :param user: `User` instance | ||
Bradley M. Kuhn
|
r4187 | :param repo_name: repository name | ||
""" | ||||
if action == 'push': | ||||
if not HasPermissionAnyMiddleware('repository.write', | ||||
Mads Kiilerich
|
r7602 | 'repository.admin')(authuser, | ||
Bradley M. Kuhn
|
r4187 | repo_name): | ||
return False | ||||
else: | ||||
#any other action need at least read permission | ||||
if not HasPermissionAnyMiddleware('repository.read', | ||||
'repository.write', | ||||
Mads Kiilerich
|
r7602 | 'repository.admin')(authuser, | ||
Bradley M. Kuhn
|
r4187 | repo_name): | ||
return False | ||||
return True | ||||
def _get_ip_addr(self, environ): | ||||
return _get_ip_addr(environ) | ||||
def __call__(self, environ, start_response): | ||||
try: | ||||
Mads Kiilerich
|
r7630 | # try parsing a request for this VCS - if it fails, call the wrapped app | ||
Mads Kiilerich
|
r7624 | parsed_request = self.parse_request(environ) | ||
if parsed_request is None: | ||||
return self.application(environ, start_response) | ||||
Mads Kiilerich
|
r7630 | |||
# skip passing error to error controller | ||||
environ['pylons.status_code_redirect'] = True | ||||
# quick check if repo exists... | ||||
if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias): | ||||
raise webob.exc.HTTPNotFound() | ||||
if parsed_request.action is None: | ||||
# Note: the client doesn't get the helpful error message | ||||
raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name) | ||||
#====================================================================== | ||||
# CHECK PERMISSIONS | ||||
#====================================================================== | ||||
ip_addr = self._get_ip_addr(environ) | ||||
user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr) | ||||
if response_app is not None: | ||||
return response_app(environ, start_response) | ||||
#====================================================================== | ||||
# REQUEST HANDLING | ||||
#====================================================================== | ||||
Mads Kiilerich
|
r7634 | set_hook_environment(user.username, ip_addr, | ||
Mads Kiilerich
|
r7633 | parsed_request.repo_name, self.scm_alias, parsed_request.action) | ||
Mads Kiilerich
|
r7630 | |||
try: | ||||
log.info('%s action on %s repo "%s" by "%s" from %s', | ||||
Mads Kiilerich
|
r8076 | parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr) | ||
Mads Kiilerich
|
r7630 | app = self._make_app(parsed_request) | ||
return app(environ, start_response) | ||||
except Exception: | ||||
log.error(traceback.format_exc()) | ||||
raise webob.exc.HTTPInternalServerError() | ||||
Mads Kiilerich
|
r7625 | except webob.exc.HTTPException as e: | ||
return e(environ, start_response) | ||||
Bradley M. Kuhn
|
r4187 | |||
Alessandro Molina
|
r6522 | class BaseController(TGController): | ||
Bradley M. Kuhn
|
r4187 | |||
Thomas De Schampheleire
|
r6513 | def _before(self, *args, **kwargs): | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Alessandro Molina
|
r6522 | _before is called before controller methods and after __call__ | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Mads Kiilerich
|
r7609 | if request.needs_csrf_check: | ||
# CSRF protection: Whenever a request has ambient authority (whether | ||||
# through a session cookie or its origin IP address), it must include | ||||
# the correct token, unless the HTTP method is GET or HEAD (and thus | ||||
# guaranteed to be side effect free. In practice, the only situation | ||||
# where we allow side effects without ambient authority is when the | ||||
# authority comes from an API key; and that is handled above. | ||||
Mads Kiilerich
|
r7708 | from kallithea.lib import helpers as h | ||
Mads Kiilerich
|
r7709 | token = request.POST.get(h.session_csrf_secret_name) | ||
if not token or token != h.session_csrf_secret_token(): | ||||
Mads Kiilerich
|
r7609 | log.error('CSRF check failed') | ||
raise webob.exc.HTTPForbidden() | ||||
Bradley M. Kuhn
|
r4200 | c.kallithea_version = __version__ | ||
Bradley M. Kuhn
|
r4203 | rc_config = Setting.get_app_settings() | ||
Bradley M. Kuhn
|
r4187 | |||
# Visual options | ||||
c.visual = AttributeDict({}) | ||||
## DB stored | ||||
Bradley M. Kuhn
|
r4218 | c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon')) | ||
c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon')) | ||||
domruf
|
r7032 | c.visual.stylify_metalabels = str2bool(rc_config.get('stylify_metalabels')) | ||
Mads Kiilerich
|
r6274 | c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100)) | ||
Bradley M. Kuhn
|
r4218 | c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100)) | ||
c.visual.repository_fields = str2bool(rc_config.get('repository_fields')) | ||||
c.visual.show_version = str2bool(rc_config.get('show_version')) | ||||
c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar')) | ||||
c.visual.gravatar_url = rc_config.get('gravatar_url') | ||||
Bradley M. Kuhn
|
r4187 | |||
Bradley M. Kuhn
|
r4218 | c.ga_code = rc_config.get('ga_code') | ||
domruf
|
r4462 | # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code | ||
if c.ga_code and '<' not in c.ga_code: | ||||
c.ga_code = '''<script type="text/javascript"> | ||||
var _gaq = _gaq || []; | ||||
_gaq.push(['_setAccount', '%s']); | ||||
_gaq.push(['_trackPageview']); | ||||
(function() { | ||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; | ||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; | ||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); | ||||
})(); | ||||
</script>''' % c.ga_code | ||||
Bradley M. Kuhn
|
r4218 | c.site_name = rc_config.get('title') | ||
Mads Kiilerich
|
r7667 | c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI | ||
domruf
|
r7688 | c.clone_ssh_tmpl = rc_config.get('clone_ssh_tmpl') or Repository.DEFAULT_CLONE_SSH | ||
Bradley M. Kuhn
|
r4187 | |||
## INI stored | ||||
c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True)) | ||||
c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True)) | ||||
Thomas De Schampheleire
|
r7677 | c.ssh_enabled = str2bool(config.get('ssh_enabled', False)) | ||
Bradley M. Kuhn
|
r4187 | |||
Bradley M. Kuhn
|
r4199 | c.instance_id = config.get('instance_id') | ||
Bradley M. Kuhn
|
r4194 | c.issues_url = config.get('bugtracker', url('issues_url')) | ||
Bradley M. Kuhn
|
r4187 | # END CONFIG VARS | ||
c.repo_name = get_repo_slug(request) # can be empty | ||||
Mads Kiilerich
|
r7906 | c.backends = list(BACKENDS) | ||
Bradley M. Kuhn
|
r4187 | |||
self.cut_off_limit = safe_int(config.get('cut_off_limit')) | ||||
Mads Kiilerich
|
r4281 | |||
Mads Kiilerich
|
r6412 | c.my_pr_count = PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count() | ||
Mads Kiilerich
|
r4281 | |||
Søren Løvborg
|
r6483 | self.scm_model = ScmModel() | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r5192 | @staticmethod | ||
Mads Kiilerich
|
r7608 | def _determine_auth_user(session_authuser, ip_addr): | ||
Mads Kiilerich
|
r5192 | """ | ||
Søren Løvborg
|
r6347 | Create an `AuthUser` object given the API key/bearer token | ||
(if any) and the value of the authuser session cookie. | ||||
Mads Kiilerich
|
r7603 | Returns None if no valid user is found (like not active or no access for IP). | ||
Mads Kiilerich
|
r5192 | """ | ||
Søren Løvborg
|
r5254 | # Authenticate by session cookie | ||
Søren Løvborg
|
r5265 | # In ancient login sessions, 'authuser' may not be a dict. | ||
# In that case, the user will have to log in again. | ||||
Søren Løvborg
|
r5548 | # v0.3 and earlier included an 'is_authenticated' key; if present, | ||
# this must be True. | ||||
if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True): | ||||
Mads Kiilerich
|
r7603 | return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr) | ||
Søren Løvborg
|
r5255 | |||
Søren Løvborg
|
r5264 | # Authenticate by auth_container plugin (if enabled) | ||
if any( | ||||
domruf
|
r6517 | plugin.is_container_auth | ||
for plugin in auth_modules.get_auth_plugins() | ||||
Søren Løvborg
|
r5264 | ): | ||
try: | ||||
Mads Kiilerich
|
r5338 | user_info = auth_modules.authenticate('', '', request.environ) | ||
Søren Løvborg
|
r5264 | except UserCreationError as e: | ||
from kallithea.lib import helpers as h | ||||
h.flash(e, 'error', logf=log.error) | ||||
else: | ||||
Mads Kiilerich
|
r5338 | if user_info is not None: | ||
username = user_info['username'] | ||||
Søren Løvborg
|
r5264 | user = User.get_by_username(username, case_insensitive=True) | ||
Mads Kiilerich
|
r7603 | return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr) | ||
Søren Løvborg
|
r5264 | |||
Mads Kiilerich
|
r7601 | # User is default user (if active) or anonymous | ||
default_user = User.get_default_user(cache=True) | ||||
Mads Kiilerich
|
r7603 | authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) | ||
Mads Kiilerich
|
r7602 | if authuser is None: # fall back to anonymous | ||
authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make? | ||||
return authuser | ||||
Mads Kiilerich
|
r5192 | |||
Søren Løvborg
|
r6344 | @staticmethod | ||
def _basic_security_checks(): | ||||
"""Perform basic security/sanity checks before processing the request.""" | ||||
# Only allow the following HTTP request methods. | ||||
if request.method not in ['GET', 'HEAD', 'POST']: | ||||
raise webob.exc.HTTPMethodNotAllowed() | ||||
# Also verify the _method override - no longer allowed. | ||||
if request.params.get('_method') is None: | ||||
pass # no override, no problem | ||||
else: | ||||
raise webob.exc.HTTPMethodNotAllowed() | ||||
Mads Kiilerich
|
r5192 | |||
Søren Løvborg
|
r6344 | # Make sure CSRF token never appears in the URL. If so, invalidate it. | ||
Mads Kiilerich
|
r7708 | from kallithea.lib import helpers as h | ||
Mads Kiilerich
|
r7709 | if h.session_csrf_secret_name in request.GET: | ||
Søren Løvborg
|
r6344 | log.error('CSRF key leak detected') | ||
Mads Kiilerich
|
r7709 | session.pop(h.session_csrf_secret_name, None) | ||
Søren Løvborg
|
r6344 | session.save() | ||
h.flash(_('CSRF token leak has been detected - all form tokens have been expired'), | ||||
category='error') | ||||
# WebOb already ignores request payload parameters for anything other | ||||
# than POST/PUT, but double-check since other Kallithea code relies on | ||||
# this assumption. | ||||
if request.method not in ['POST', 'PUT'] and request.POST: | ||||
log.error('%r request with payload parameters; WebOb should have stopped this', request.method) | ||||
raise webob.exc.HTTPBadRequest() | ||||
Alessandro Molina
|
r6522 | def __call__(self, environ, context): | ||
Bradley M. Kuhn
|
r4187 | try: | ||
Mads Kiilerich
|
r7603 | ip_addr = _get_ip_addr(environ) | ||
Søren Løvborg
|
r6344 | self._basic_security_checks() | ||
Mads Kiilerich
|
r7608 | api_key = request.GET.get('api_key') | ||
Søren Løvborg
|
r6347 | try: | ||
# Request.authorization may raise ValueError on invalid input | ||||
type, params = request.authorization | ||||
except (ValueError, TypeError): | ||||
pass | ||||
else: | ||||
if type.lower() == 'bearer': | ||||
Mads Kiilerich
|
r7608 | api_key = params # bearer token is an api key too | ||
if api_key is None: | ||||
authuser = self._determine_auth_user( | ||||
session.get('authuser'), | ||||
ip_addr=ip_addr, | ||||
) | ||||
Mads Kiilerich
|
r7609 | needs_csrf_check = request.method not in ['GET', 'HEAD'] | ||
Søren Løvborg
|
r6347 | |||
Mads Kiilerich
|
r7608 | else: | ||
dbuser = User.get_by_api_key(api_key) | ||||
if dbuser is None: | ||||
log.info('No db user found for authentication with API key ****%s from %s', | ||||
api_key[-4:], ip_addr) | ||||
Mads Kiilerich
|
r7610 | authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr) | ||
Mads Kiilerich
|
r7609 | needs_csrf_check = False # API key provides CSRF protection | ||
Mads Kiilerich
|
r7608 | |||
Mads Kiilerich
|
r7602 | if authuser is None: | ||
log.info('No valid user found') | ||||
raise webob.exc.HTTPForbidden() | ||||
Mads Kiilerich
|
r7406 | |||
Mads Kiilerich
|
r7608 | # set globals for auth user | ||
Mads Kiilerich
|
r7406 | request.authuser = authuser | ||
Mads Kiilerich
|
r7603 | request.ip_addr = ip_addr | ||
Mads Kiilerich
|
r7609 | request.needs_csrf_check = needs_csrf_check | ||
Mads Kiilerich
|
r7406 | |||
Mads Kiilerich
|
r5192 | log.info('IP: %s User: %s accessed %s', | ||
Mads Kiilerich
|
r6412 | request.ip_addr, request.authuser, | ||
Mads Kiilerich
|
r7948 | get_path_info(environ), | ||
Bradley M. Kuhn
|
r4187 | ) | ||
Alessandro Molina
|
r6522 | return super(BaseController, self).__call__(environ, context) | ||
Søren Løvborg
|
r6344 | except webob.exc.HTTPException as e: | ||
Alessandro Molina
|
r6522 | return e | ||
Bradley M. Kuhn
|
r4187 | |||
class BaseRepoController(BaseController): | ||||
""" | ||||
Base class for controllers responsible for loading all needed data for | ||||
repository loaded items are | ||||
Bradley M. Kuhn
|
r4196 | c.db_repo_scm_instance: instance of scm repository | ||
Bradley M. Kuhn
|
r4195 | c.db_repo: instance of db | ||
Bradley M. Kuhn
|
r4187 | c.repository_followers: number of followers | ||
c.repository_forks: number of forks | ||||
c.repository_following: weather the current user is following the current repo | ||||
""" | ||||
Thomas De Schampheleire
|
r6513 | def _before(self, *args, **kwargs): | ||
super(BaseRepoController, self)._before(*args, **kwargs) | ||||
Bradley M. Kuhn
|
r4187 | if c.repo_name: # extracted from routes | ||
_dbr = Repository.get_by_repo_name(c.repo_name) | ||||
if not _dbr: | ||||
return | ||||
Mads Kiilerich
|
r5375 | log.debug('Found repository in database %s with state `%s`', | ||
Mads Kiilerich
|
r8075 | _dbr, _dbr.repo_state) | ||
Bradley M. Kuhn
|
r4187 | route = getattr(request.environ.get('routes.route'), 'name', '') | ||
# allow to delete repos that are somehow damages in filesystem | ||||
if route in ['delete_repo']: | ||||
return | ||||
if _dbr.repo_state in [Repository.STATE_PENDING]: | ||||
if route in ['repo_creating_home']: | ||||
return | ||||
check_url = url('repo_creating_home', repo_name=c.repo_name) | ||||
Søren Løvborg
|
r5543 | raise webob.exc.HTTPFound(location=check_url) | ||
Bradley M. Kuhn
|
r4187 | |||
Bradley M. Kuhn
|
r4195 | dbr = c.db_repo = _dbr | ||
Bradley M. Kuhn
|
r4196 | c.db_repo_scm_instance = c.db_repo.scm_instance | ||
if c.db_repo_scm_instance is None: | ||||
Bradley M. Kuhn
|
r4187 | log.error('%s this repository is present in database but it ' | ||
'cannot be created as an scm instance', c.repo_name) | ||||
Mads Kiilerich
|
r4363 | from kallithea.lib import helpers as h | ||
Thomas De Schampheleire
|
r7467 | h.flash(_('Repository not found in the filesystem'), | ||
Mads Kiilerich
|
r4363 | category='error') | ||
Thomas De Schampheleire
|
r7415 | raise webob.exc.HTTPNotFound() | ||
Bradley M. Kuhn
|
r4187 | |||
# some globals counter for menu | ||||
c.repository_followers = self.scm_model.get_followers(dbr) | ||||
c.repository_forks = self.scm_model.get_forks(dbr) | ||||
c.repository_pull_requests = self.scm_model.get_pull_requests(dbr) | ||||
c.repository_following = self.scm_model.is_following_repo( | ||||
Mads Kiilerich
|
r6412 | c.repo_name, request.authuser.user_id) | ||
Mads Kiilerich
|
r4364 | |||
@staticmethod | ||||
Mads Kiilerich
|
r4506 | def _get_ref_rev(repo, ref_type, ref_name, returnempty=False): | ||
Mads Kiilerich
|
r4364 | """ | ||
Safe way to get changeset. If error occurs show error. | ||||
""" | ||||
from kallithea.lib import helpers as h | ||||
try: | ||||
return repo.scm_instance.get_ref_revision(ref_type, ref_name) | ||||
except EmptyRepositoryError as e: | ||||
Mads Kiilerich
|
r4506 | if returnempty: | ||
return repo.scm_instance.EMPTY_CHANGESET | ||||
Thomas De Schampheleire
|
r7467 | h.flash(_('There are no changesets yet'), category='error') | ||
Mads Kiilerich
|
r4364 | raise webob.exc.HTTPNotFound() | ||
except ChangesetDoesNotExistError as e: | ||||
Thomas De Schampheleire
|
r7467 | h.flash(_('Changeset for %s %s not found in %s') % | ||
(ref_type, ref_name, repo.repo_name), | ||||
Mads Kiilerich
|
r4364 | category='error') | ||
raise webob.exc.HTTPNotFound() | ||||
except RepositoryError as e: | ||||
log.error(traceback.format_exc()) | ||||
Mads Kiilerich
|
r7949 | h.flash(e, category='error') | ||
Mads Kiilerich
|
r4364 | raise webob.exc.HTTPBadRequest() | ||
Mads Kiilerich
|
r5414 | |||
Thomas De Schampheleire
|
r6409 | @decorator.decorator | ||
def jsonify(func, *args, **kwargs): | ||||
"""Action decorator that formats output for JSON | ||||
Mads Kiilerich
|
r5414 | |||
Thomas De Schampheleire
|
r6409 | Given a function that will return content, this decorator will turn | ||
the result into JSON, with a content-type of 'application/json' and | ||||
output it. | ||||
""" | ||||
response.headers['Content-Type'] = 'application/json; charset=utf-8' | ||||
data = func(*args, **kwargs) | ||||
if isinstance(data, (list, tuple)): | ||||
# A JSON list response is syntactically valid JavaScript and can be | ||||
# loaded and executed as JavaScript by a malicious third-party site | ||||
# using <script>, which can lead to cross-site data leaks. | ||||
# JSON responses should therefore be scalars or objects (i.e. Python | ||||
# dicts), because a JSON object is a syntax error if intepreted as JS. | ||||
msg = "JSON responses with Array envelopes are susceptible to " \ | ||||
"cross-site data leak attacks, see " \ | ||||
"https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings" | ||||
warnings.warn(msg, Warning, 2) | ||||
log.warning(msg) | ||||
log.debug("Returning JSON wrapped action output") | ||||
Mads Kiilerich
|
r7987 | return ascii_bytes(ext_json.dumps(data)) | ||
Thomas De Schampheleire
|
r7677 | |||
@decorator.decorator | ||||
def IfSshEnabled(func, *args, **kwargs): | ||||
"""Decorator for functions that can only be called if SSH access is enabled. | ||||
If SSH access is disabled in the configuration file, HTTPNotFound is raised. | ||||
""" | ||||
if not c.ssh_enabled: | ||||
from kallithea.lib import helpers as h | ||||
h.flash(_("SSH access is disabled."), category='warning') | ||||
raise webob.exc.HTTPNotFound() | ||||
return func(*args, **kwargs) | ||||