# HG changeset patch # User Mads Kiilerich # Date 1605225976 -3600 # Node ID 4f0de9468da35290c78f8b661f9662b7850f7e11 # Parent 9a2c0a067c6e7cdf7a49c210f8841fb25d9d2a9c controllers: move controllers base class from lib/base to controllers TG quickstart put it in lib/base.py , but it fits better on the controllers layer as a base there. The contributing docs were a bit ahead of time ... but with a typo. diff -r 9a2c0a067c6e -r 4f0de9468da3 docs/contributing.rst --- a/docs/contributing.rst Mon Dec 14 23:00:25 2020 +0100 +++ b/docs/contributing.rst Fri Nov 13 01:06:16 2020 +0100 @@ -112,7 +112,7 @@ everything and has a huge dependency chain, so it should not be used for anything else. TODO. -``controlles/base.py`` +``controllers/base.py`` The base class of controllers, with lots of model knowledge. ``lib/auth.py`` diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/config/middleware/simplegit.py --- a/kallithea/config/middleware/simplegit.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/config/middleware/simplegit.py Fri Nov 13 01:06:16 2020 +0100 @@ -32,8 +32,8 @@ import re from kallithea.config.middleware.pygrack import make_wsgi_app +from kallithea.controllers import base from kallithea.lib import hooks -from kallithea.lib.base import BaseVCSController, get_path_info log = logging.getLogger(__name__) @@ -48,13 +48,13 @@ } -class SimpleGit(BaseVCSController): +class SimpleGit(base.BaseVCSController): scm_alias = 'git' @classmethod def parse_request(cls, environ): - path_info = get_path_info(environ) + path_info = base.get_path_info(environ) m = GIT_PROTO_PAT.match(path_info) if m is None: return None diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/config/middleware/simplehg.py --- a/kallithea/config/middleware/simplehg.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/config/middleware/simplehg.py Fri Nov 13 01:06:16 2020 +0100 @@ -34,7 +34,7 @@ import mercurial.hgweb -from kallithea.lib.base import BaseVCSController, get_path_info +from kallithea.controllers import base from kallithea.lib.utils import make_ui from kallithea.lib.utils2 import safe_bytes @@ -91,7 +91,7 @@ } -class SimpleHg(BaseVCSController): +class SimpleHg(base.BaseVCSController): scm_alias = 'hg' @@ -100,7 +100,7 @@ http_accept = environ.get('HTTP_ACCEPT', '') if not http_accept.startswith('application/mercurial'): return None - path_info = get_path_info(environ) + path_info = base.get_path_info(environ) if not path_info.startswith('/'): # it must! return None diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/config/middleware/wrapper.py --- a/kallithea/config/middleware/wrapper.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/config/middleware/wrapper.py Fri Nov 13 01:06:16 2020 +0100 @@ -29,7 +29,7 @@ import logging import time -from kallithea.lib.base import get_ip_addr, get_path_info +from kallithea.controllers import base log = logging.getLogger(__name__) @@ -91,8 +91,8 @@ def __call__(self, environ, start_response): meter = Meter(start_response) description = "Request from %s for %s" % ( - get_ip_addr(environ), - get_path_info(environ), + base.get_ip_addr(environ), + base.get_path_info(environ), ) log.info("%s received", description) try: diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/admin.py --- a/kallithea/controllers/admin/admin.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/admin.py Fri Nov 13 01:06:16 2020 +0100 @@ -36,8 +36,8 @@ from whoosh.qparser.dateparse import DateParserPlugin from whoosh.qparser.default import QueryParser +from kallithea.controllers import base from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired -from kallithea.lib.base import BaseController, render from kallithea.lib.indexers import JOURNAL_SCHEMA from kallithea.lib.page import Page from kallithea.lib.utils2 import remove_prefix, remove_suffix, safe_int @@ -118,7 +118,7 @@ return user_log -class AdminController(BaseController): +class AdminController(base.BaseController): @LoginRequired(allow_default_user=True) def _before(self, *args, **kwargs): @@ -142,6 +142,6 @@ filter=c.search_term) if request.environ.get('HTTP_X_PARTIAL_XHR'): - return render('admin/admin_log.html') + return base.render('admin/admin_log.html') - return render('admin/admin.html') + return base.render('admin/admin.html') diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/auth_settings.py --- a/kallithea/controllers/admin/auth_settings.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/auth_settings.py Fri Nov 13 01:06:16 2020 +0100 @@ -32,9 +32,9 @@ from tg.i18n import ugettext as _ from webob.exc import HTTPFound +from kallithea.controllers import base from kallithea.lib import auth_modules, webutils from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired -from kallithea.lib.base import BaseController, render from kallithea.lib.webutils import url from kallithea.model import db, meta from kallithea.model.forms import AuthSettingsForm @@ -43,7 +43,7 @@ log = logging.getLogger(__name__) -class AuthSettingsController(BaseController): +class AuthSettingsController(base.BaseController): @LoginRequired() @HasPermissionAnyDecorator('hg.admin') @@ -86,7 +86,7 @@ log.debug('defaults: %s', defaults) return formencode.htmlfill.render( - render('admin/auth/auth_settings.html'), + base.render('admin/auth/auth_settings.html'), defaults=c.defaults, errors=errors, prefix_error=False, diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/defaults.py --- a/kallithea/controllers/admin/defaults.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/defaults.py Fri Nov 13 01:06:16 2020 +0100 @@ -34,9 +34,9 @@ from tg.i18n import ugettext as _ from webob.exc import HTTPFound +from kallithea.controllers import base from kallithea.lib import webutils from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired -from kallithea.lib.base import BaseController, render from kallithea.lib.webutils import url from kallithea.model import db, meta from kallithea.model.forms import DefaultsForm @@ -45,7 +45,7 @@ log = logging.getLogger(__name__) -class DefaultsController(BaseController): +class DefaultsController(base.BaseController): @LoginRequired() @HasPermissionAnyDecorator('hg.admin') @@ -56,7 +56,7 @@ defaults = db.Setting.get_default_repo_settings() return htmlfill.render( - render('admin/defaults/defaults.html'), + base.render('admin/defaults/defaults.html'), defaults=defaults, encoding="UTF-8", force_defaults=False @@ -77,7 +77,7 @@ defaults = errors.value return htmlfill.render( - render('admin/defaults/defaults.html'), + base.render('admin/defaults/defaults.html'), defaults=defaults, errors=errors.error_dict or {}, prefix_error=False, diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/gists.py --- a/kallithea/controllers/admin/gists.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/gists.py Fri Nov 13 01:06:16 2020 +0100 @@ -35,9 +35,9 @@ from tg.i18n import ugettext as _ from webob.exc import HTTPForbidden, HTTPFound, HTTPNotFound +from kallithea.controllers import base from kallithea.lib import auth, webutils from kallithea.lib.auth import LoginRequired -from kallithea.lib.base import BaseController, jsonify, render from kallithea.lib.page import Page from kallithea.lib.utils2 import safe_int, safe_str, time_to_datetime from kallithea.lib.vcs.exceptions import NodeNotChangedError, VCSError @@ -50,7 +50,7 @@ log = logging.getLogger(__name__) -class GistsController(BaseController): +class GistsController(base.BaseController): def __load_defaults(self, extra_values=None): c.lifetime_values = [ @@ -102,7 +102,7 @@ p = safe_int(request.GET.get('page'), 1) c.gists_pager = Page(c.gists, page=p, items_per_page=10, **url_params) - return render('admin/gists/index.html') + return base.render('admin/gists/index.html') @LoginRequired() def create(self): @@ -134,7 +134,7 @@ defaults = errors.value return formencode.htmlfill.render( - render('admin/gists/new.html'), + base.render('admin/gists/new.html'), defaults=defaults, errors=errors.error_dict or {}, prefix_error=False, @@ -150,7 +150,7 @@ @LoginRequired() def new(self, format='html'): self.__load_defaults() - return render('admin/gists/new.html') + return base.render('admin/gists/new.html') @LoginRequired() def delete(self, gist_id): @@ -186,7 +186,7 @@ ) response.content_type = 'text/plain' return content - return render('admin/gists/show.html') + return base.render('admin/gists/show.html') @LoginRequired() def edit(self, gist_id, format='html'): @@ -203,7 +203,7 @@ raise HTTPNotFound() self.__load_defaults(extra_values=('0', _('Unmodified'))) - rendered = render('admin/gists/edit.html') + rendered = base.render('admin/gists/edit.html') if request.POST: rpost = request.POST @@ -248,7 +248,7 @@ return rendered @LoginRequired() - @jsonify + @base.jsonify def check_revision(self, gist_id): c.gist = db.Gist.get_or_404(gist_id) last_rev = c.gist.scm_instance.get_changeset() diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/my_account.py --- a/kallithea/controllers/admin/my_account.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/my_account.py Fri Nov 13 01:06:16 2020 +0100 @@ -35,9 +35,9 @@ from tg.i18n import ugettext as _ from webob.exc import HTTPFound +from kallithea.controllers import base from kallithea.lib import auth_modules, webutils from kallithea.lib.auth import AuthUser, LoginRequired -from kallithea.lib.base import BaseController, IfSshEnabled, render from kallithea.lib.utils2 import generate_api_key, safe_int from kallithea.lib.webutils import url from kallithea.model import db, meta @@ -51,7 +51,7 @@ log = logging.getLogger(__name__) -class MyAccountController(BaseController): +class MyAccountController(base.BaseController): @LoginRequired() def _before(self, *args, **kwargs): @@ -116,7 +116,7 @@ except formencode.Invalid as errors: return htmlfill.render( - render('admin/my_account/my_account.html'), + base.render('admin/my_account/my_account.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -129,7 +129,7 @@ if update: raise HTTPFound(location='my_account') return htmlfill.render( - render('admin/my_account/my_account.html'), + base.render('admin/my_account/my_account.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -150,7 +150,7 @@ webutils.flash(_("Successfully updated password"), category='success') except formencode.Invalid as errors: return htmlfill.render( - render('admin/my_account/my_account.html'), + base.render('admin/my_account/my_account.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -160,7 +160,7 @@ log.error(traceback.format_exc()) webutils.flash(_('Error occurred during update of user password'), category='error') - return render('admin/my_account/my_account.html') + return base.render('admin/my_account/my_account.html') def my_account_repos(self): c.active = 'repos' @@ -168,7 +168,7 @@ # data used to render the grid c.data = self._load_my_repos_data() - return render('admin/my_account/my_account.html') + return base.render('admin/my_account/my_account.html') def my_account_watched(self): c.active = 'watched' @@ -176,14 +176,14 @@ # data used to render the grid c.data = self._load_my_repos_data(watched=True) - return render('admin/my_account/my_account.html') + return base.render('admin/my_account/my_account.html') def my_account_perms(self): c.active = 'perms' self.__load_data() c.perm_user = AuthUser(user_id=request.authuser.user_id) - return render('admin/my_account/my_account.html') + return base.render('admin/my_account/my_account.html') def my_account_emails(self): c.active = 'emails' @@ -191,7 +191,7 @@ c.user_email_map = db.UserEmailMap.query() \ .filter(db.UserEmailMap.user == c.user).all() - return render('admin/my_account/my_account.html') + return base.render('admin/my_account/my_account.html') def my_account_emails_add(self): email = request.POST.get('new_email') @@ -231,7 +231,7 @@ c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] c.user_api_keys = ApiKeyModel().get_api_keys(request.authuser.user_id, show_expired=show_expired) - return render('admin/my_account/my_account.html') + return base.render('admin/my_account/my_account.html') def my_account_api_keys_add(self): lifetime = safe_int(request.POST.get('lifetime'), -1) @@ -255,14 +255,14 @@ raise HTTPFound(location=url('my_account_api_keys')) - @IfSshEnabled + @base.IfSshEnabled def my_account_ssh_keys(self): c.active = 'ssh_keys' self.__load_data() c.user_ssh_keys = SshKeyModel().get_ssh_keys(request.authuser.user_id) - return render('admin/my_account/my_account.html') + return base.render('admin/my_account/my_account.html') - @IfSshEnabled + @base.IfSshEnabled def my_account_ssh_keys_add(self): description = request.POST.get('description') public_key = request.POST.get('public_key') @@ -276,7 +276,7 @@ webutils.flash(e.args[0], category='error') raise HTTPFound(location=url('my_account_ssh_keys')) - @IfSshEnabled + @base.IfSshEnabled def my_account_ssh_keys_delete(self): fingerprint = request.POST.get('del_public_key_fingerprint') try: diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/permissions.py --- a/kallithea/controllers/admin/permissions.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/permissions.py Fri Nov 13 01:06:16 2020 +0100 @@ -36,9 +36,9 @@ from tg.i18n import ugettext as _ from webob.exc import HTTPFound +from kallithea.controllers import base from kallithea.lib import webutils from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired -from kallithea.lib.base import BaseController, render from kallithea.lib.webutils import url from kallithea.model import db, meta from kallithea.model.forms import DefaultPermissionsForm @@ -48,7 +48,7 @@ log = logging.getLogger(__name__) -class PermissionsController(BaseController): +class PermissionsController(base.BaseController): @LoginRequired() @HasPermissionAnyDecorator('hg.admin') @@ -120,7 +120,7 @@ defaults = errors.value return htmlfill.render( - render('admin/permissions/permissions.html'), + base.render('admin/permissions/permissions.html'), defaults=defaults, errors=errors.error_dict or {}, prefix_error=False, @@ -162,7 +162,7 @@ defaults['default_fork'] = p.permission.permission_name return htmlfill.render( - render('admin/permissions/permissions.html'), + base.render('admin/permissions/permissions.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -173,10 +173,10 @@ c.user_ip_map = db.UserIpMap.query() \ .filter(db.UserIpMap.user == c.user).all() - return render('admin/permissions/permissions.html') + return base.render('admin/permissions/permissions.html') def permission_perms(self): c.active = 'perms' c.user = db.User.get_default_user() c.perm_user = AuthUser(dbuser=c.user) - return render('admin/permissions/permissions.html') + return base.render('admin/permissions/permissions.html') diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/repo_groups.py --- a/kallithea/controllers/admin/repo_groups.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/repo_groups.py Fri Nov 13 01:06:16 2020 +0100 @@ -37,9 +37,9 @@ from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.lib import webutils from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoGroupPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseController, render from kallithea.lib.utils2 import safe_int from kallithea.lib.webutils import url from kallithea.model import db, meta @@ -52,7 +52,7 @@ log = logging.getLogger(__name__) -class RepoGroupsController(BaseController): +class RepoGroupsController(base.BaseController): @LoginRequired(allow_default_user=True) def _before(self, *args, **kwargs): @@ -132,7 +132,7 @@ "records": repo_groups_data } - return render('admin/repo_groups/repo_groups.html') + return base.render('admin/repo_groups/repo_groups.html') def create(self): self.__load_defaults() @@ -154,7 +154,7 @@ # TODO: in future action_logger(, '', '', '') except formencode.Invalid as errors: return htmlfill.render( - render('admin/repo_groups/repo_group_add.html'), + base.render('admin/repo_groups/repo_group_add.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -190,7 +190,7 @@ self.__load_defaults() return htmlfill.render( - render('admin/repo_groups/repo_group_add.html'), + base.render('admin/repo_groups/repo_group_add.html'), defaults={'parent_group_id': parent_group_id}, errors={}, prefix_error=False, @@ -230,7 +230,7 @@ except formencode.Invalid as errors: c.active = 'settings' return htmlfill.render( - render('admin/repo_groups/repo_group_edit.html'), + base.render('admin/repo_groups/repo_group_edit.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -298,7 +298,7 @@ repo_groups_list=repo_groups_list, short_name=True) - return render('admin/repo_groups/repo_group_show.html') + return base.render('admin/repo_groups/repo_group_show.html') @HasRepoGroupPermissionLevelDecorator('admin') def edit(self, group_name): @@ -310,7 +310,7 @@ defaults = self.__load_data(c.repo_group.group_id) return htmlfill.render( - render('admin/repo_groups/repo_group_edit.html'), + base.render('admin/repo_groups/repo_group_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False @@ -321,7 +321,7 @@ c.active = 'advanced' c.repo_group = db.RepoGroup.guess_instance(group_name) - return render('admin/repo_groups/repo_group_edit.html') + return base.render('admin/repo_groups/repo_group_edit.html') @HasRepoGroupPermissionLevelDecorator('admin') def edit_repo_group_perms(self, group_name): @@ -331,7 +331,7 @@ defaults = self.__load_data(c.repo_group.group_id) return htmlfill.render( - render('admin/repo_groups/repo_group_edit.html'), + base.render('admin/repo_groups/repo_group_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/repos.py --- a/kallithea/controllers/admin/repos.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/repos.py Fri Nov 13 01:06:16 2020 +0100 @@ -36,9 +36,9 @@ from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound import kallithea +from kallithea.controllers import base from kallithea.lib import webutils from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired, NotAnonymous -from kallithea.lib.base import BaseRepoController, jsonify, render from kallithea.lib.exceptions import AttachedForksError from kallithea.lib.utils2 import safe_int from kallithea.lib.vcs import RepositoryError @@ -52,7 +52,7 @@ log = logging.getLogger(__name__) -class ReposController(BaseRepoController): +class ReposController(base.BaseRepoController): @LoginRequired(allow_default_user=True) def _before(self, *args, **kwargs): @@ -93,7 +93,7 @@ # data used to render the grid c.data = repos_data - return render('admin/repos/repos.html') + return base.render('admin/repos/repos.html') @NotAnonymous() def create(self): @@ -106,7 +106,7 @@ except formencode.Invalid as errors: log.info(errors) return htmlfill.render( - render('admin/repos/repo_add.html'), + base.render('admin/repos/repo_add.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -148,7 +148,7 @@ defaults.update({'repo_group': parent_group}) return htmlfill.render( - render('admin/repos/repo_add.html'), + base.render('admin/repos/repo_add.html'), defaults=defaults, errors={}, prefix_error=False, @@ -161,10 +161,10 @@ c.task_id = request.GET.get('task_id') if not c.repo: raise HTTPNotFound() - return render('admin/repos/repo_creating.html') + return base.render('admin/repos/repo_creating.html') @LoginRequired() - @jsonify + @base.jsonify def repo_check(self, repo_name): c.repo = repo_name task_id = request.GET.get('task_id') @@ -230,7 +230,7 @@ defaults = self.__load_data() defaults.update(errors.value) return htmlfill.render( - render('admin/repos/repo_edit.html'), + base.render('admin/repos/repo_edit.html'), defaults=defaults, errors=errors.error_dict or {}, prefix_error=False, @@ -286,7 +286,7 @@ .filter(db.RepositoryField.repository == c.repo_info).all() c.active = 'settings' return htmlfill.render( - render('admin/repos/repo_edit.html'), + base.render('admin/repos/repo_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -298,7 +298,7 @@ defaults = RepoModel()._get_defaults(repo_name) return htmlfill.render( - render('admin/repos/repo_edit.html'), + base.render('admin/repos/repo_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -355,7 +355,7 @@ if request.POST: raise HTTPFound(location=url('repo_edit_fields')) - return render('admin/repos/repo_edit.html') + return base.render('admin/repos/repo_edit.html') @HasRepoPermissionLevelDecorator('admin') def create_repo_field(self, repo_name): @@ -413,7 +413,7 @@ if request.POST: raise HTTPFound(location=url('repo_edit_advanced')) return htmlfill.render( - render('admin/repos/repo_edit.html'), + base.render('admin/repos/repo_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -478,7 +478,7 @@ webutils.flash(_('An error occurred during pull from remote location'), category='error') raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name)) - return render('admin/repos/repo_edit.html') + return base.render('admin/repos/repo_edit.html') @HasRepoPermissionLevelDecorator('admin') def edit_statistics(self, repo_name): @@ -510,4 +510,4 @@ category='error') raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name)) - return render('admin/repos/repo_edit.html') + return base.render('admin/repos/repo_edit.html') diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/settings.py --- a/kallithea/controllers/admin/settings.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/settings.py Fri Nov 13 01:06:16 2020 +0100 @@ -36,9 +36,9 @@ from webob.exc import HTTPFound import kallithea +from kallithea.controllers import base from kallithea.lib import webutils from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired -from kallithea.lib.base import BaseController, render from kallithea.lib.utils import repo2db_mapper, set_app_settings from kallithea.lib.utils2 import safe_str from kallithea.lib.vcs import VCSError @@ -52,7 +52,7 @@ log = logging.getLogger(__name__) -class SettingsController(BaseController): +class SettingsController(base.BaseController): @LoginRequired(allow_default_user=True) def _before(self, *args, **kwargs): @@ -85,7 +85,7 @@ form_result = application_form.to_python(dict(request.POST)) except formencode.Invalid as errors: return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -124,7 +124,7 @@ defaults.update(self._get_hg_ui_settings()) return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -172,7 +172,7 @@ defaults.update(self._get_hg_ui_settings()) return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -186,7 +186,7 @@ form_result = application_form.to_python(dict(request.POST)) except formencode.Invalid as errors: return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -219,7 +219,7 @@ defaults.update(self._get_hg_ui_settings()) return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -233,7 +233,7 @@ form_result = application_form.to_python(dict(request.POST)) except formencode.Invalid as errors: return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -274,7 +274,7 @@ defaults.update(self._get_hg_ui_settings()) return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -312,7 +312,7 @@ c.ini = kallithea.CONFIG return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -367,7 +367,7 @@ c.custom_hooks = db.Ui.get_custom_hooks() return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -386,7 +386,7 @@ defaults.update(self._get_hg_ui_settings()) return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -404,7 +404,7 @@ setattr(c, key, val) return htmlfill.render( - render('admin/settings/settings.html'), + base.render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/user_groups.py --- a/kallithea/controllers/admin/user_groups.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/user_groups.py Fri Nov 13 01:06:16 2020 +0100 @@ -38,9 +38,9 @@ from webob.exc import HTTPFound, HTTPInternalServerError import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.lib import webutils from kallithea.lib.auth import HasPermissionAnyDecorator, HasUserGroupPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseController, render from kallithea.lib.exceptions import RepoGroupAssignmentError, UserGroupsAssignedException from kallithea.lib.utils2 import safe_int, safe_str from kallithea.lib.webutils import url @@ -53,7 +53,7 @@ log = logging.getLogger(__name__) -class UserGroupsController(BaseController): +class UserGroupsController(base.BaseController): @LoginRequired(allow_default_user=True) def _before(self, *args, **kwargs): @@ -113,7 +113,7 @@ "records": user_groups_data } - return render('admin/user_groups/user_groups.html') + return base.render('admin/user_groups/user_groups.html') @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') def create(self): @@ -134,7 +134,7 @@ meta.Session().commit() except formencode.Invalid as errors: return htmlfill.render( - render('admin/user_groups/user_group_add.html'), + base.render('admin/user_groups/user_group_add.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -149,7 +149,7 @@ @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') def new(self, format='html'): - return render('admin/user_groups/user_group_add.html') + return base.render('admin/user_groups/user_group_add.html') @HasUserGroupPermissionLevelDecorator('admin') def update(self, id): @@ -184,7 +184,7 @@ }) return htmlfill.render( - render('admin/user_groups/user_group_edit.html'), + base.render('admin/user_groups/user_group_edit.html'), defaults=defaults, errors=e, prefix_error=False, @@ -221,7 +221,7 @@ defaults = self.__load_defaults(id) return htmlfill.render( - render('admin/user_groups/user_group_edit.html'), + base.render('admin/user_groups/user_group_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False @@ -243,7 +243,7 @@ p.permission.permission_name}) return htmlfill.render( - render('admin/user_groups/user_group_edit.html'), + base.render('admin/user_groups/user_group_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False @@ -344,7 +344,7 @@ }) return htmlfill.render( - render('admin/user_groups/user_group_edit.html'), + base.render('admin/user_groups/user_group_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False @@ -394,7 +394,7 @@ c.active = 'advanced' c.group_members_obj = sorted((x.user for x in c.user_group.members), key=lambda u: u.username.lower()) - return render('admin/user_groups/user_group_edit.html') + return base.render('admin/user_groups/user_group_edit.html') @HasUserGroupPermissionLevelDecorator('admin') def edit_members(self, id): @@ -404,4 +404,4 @@ key=lambda u: u.username.lower()) c.group_members = [(x.user_id, x.username) for x in c.group_members_obj] - return render('admin/user_groups/user_group_edit.html') + return base.render('admin/user_groups/user_group_edit.html') diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/admin/users.py --- a/kallithea/controllers/admin/users.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/admin/users.py Fri Nov 13 01:06:16 2020 +0100 @@ -38,9 +38,9 @@ import kallithea import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.lib import auth_modules, webutils from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired -from kallithea.lib.base import BaseController, IfSshEnabled, render from kallithea.lib.exceptions import DefaultUserException, UserCreationError, UserOwnsReposException from kallithea.lib.utils2 import datetime_to_time, fmt_date, generate_api_key, safe_int from kallithea.lib.webutils import url @@ -54,7 +54,7 @@ log = logging.getLogger(__name__) -class UsersController(BaseController): +class UsersController(base.BaseController): @LoginRequired() @HasPermissionAnyDecorator('hg.admin') @@ -103,7 +103,7 @@ "records": users_data } - return render('admin/users/users.html') + return base.render('admin/users/users.html') def create(self): c.default_extern_type = db.User.DEFAULT_AUTH_TYPE @@ -120,7 +120,7 @@ meta.Session().commit() except formencode.Invalid as errors: return htmlfill.render( - render('admin/users/user_add.html'), + base.render('admin/users/user_add.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -137,7 +137,7 @@ def new(self, format='html'): c.default_extern_type = db.User.DEFAULT_AUTH_TYPE c.default_extern_name = '' - return render('admin/users/user_add.html') + return base.render('admin/users/user_add.html') def update(self, id): user_model = UserModel() @@ -208,7 +208,7 @@ c.perm_user = AuthUser(dbuser=user) managed_fields = auth_modules.get_managed_fields(user) c.readonly = lambda n: 'readonly' if n in managed_fields else None - return render('admin/users/user_edit.html') + return base.render('admin/users/user_edit.html') def edit(self, id, format='html'): user = self._get_user_or_raise_if_default(id) @@ -234,7 +234,7 @@ 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'), }) return htmlfill.render( - render('admin/users/user_edit.html'), + base.render('admin/users/user_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -255,7 +255,7 @@ show_expired=show_expired) defaults = c.user.get_dict() return htmlfill.render( - render('admin/users/user_edit.html'), + base.render('admin/users/user_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -302,7 +302,7 @@ 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'), }) return htmlfill.render( - render('admin/users/user_edit.html'), + base.render('admin/users/user_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -350,7 +350,7 @@ defaults = c.user.get_dict() return htmlfill.render( - render('admin/users/user_edit.html'), + base.render('admin/users/user_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -393,7 +393,7 @@ defaults = c.user.get_dict() return htmlfill.render( - render('admin/users/user_edit.html'), + base.render('admin/users/user_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -429,19 +429,19 @@ raise HTTPFound(location=url('admin_permissions_ips')) raise HTTPFound(location=url('edit_user_ips', id=id)) - @IfSshEnabled + @base.IfSshEnabled def edit_ssh_keys(self, id): c.user = self._get_user_or_raise_if_default(id) c.active = 'ssh_keys' c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id) defaults = c.user.get_dict() return htmlfill.render( - render('admin/users/user_edit.html'), + base.render('admin/users/user_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) - @IfSshEnabled + @base.IfSshEnabled def ssh_keys_add(self, id): c.user = self._get_user_or_raise_if_default(id) @@ -457,7 +457,7 @@ webutils.flash(e.args[0], category='error') raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id)) - @IfSshEnabled + @base.IfSshEnabled def ssh_keys_delete(self, id): c.user = self._get_user_or_raise_if_default(id) diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/api/__init__.py --- a/kallithea/controllers/api/__init__.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/api/__init__.py Fri Nov 13 01:06:16 2020 +0100 @@ -35,9 +35,9 @@ from tg import Response, TGController, request, response from webob.exc import HTTPError, HTTPException +from kallithea.controllers import base from kallithea.lib import ext_json from kallithea.lib.auth import AuthUser -from kallithea.lib.base import get_ip_addr, get_path_info from kallithea.lib.utils2 import ascii_bytes from kallithea.model import db @@ -99,7 +99,7 @@ environ = state.request.environ start = time.time() - ip_addr = get_ip_addr(environ) + ip_addr = base.get_ip_addr(environ) self._req_id = None if 'CONTENT_LENGTH' not in environ: log.debug("No Content-Length") @@ -204,8 +204,8 @@ self._rpc_args['environ'] = environ log.info('IP: %s Request to %s time: %.3fs' % ( - get_ip_addr(environ), - get_path_info(environ), time.time() - start) + base.get_ip_addr(environ), + base.get_path_info(environ), time.time() - start) ) state.set_action(self._rpc_call, []) diff -r 9a2c0a067c6e -r 4f0de9468da3 kallithea/controllers/base.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/controllers/base.py Fri Nov 13 01:06:16 2020 +0100 @@ -0,0 +1,639 @@ +# -*- 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 . + +""" +kallithea.controllers.base +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The base Controller API +Provides the BaseController class for subclassing. And usage in different +controllers + +This file was forked by the Kallithea project in July 2014. +Original author and date, and relevant copyright and licensing information is below: +:created_on: Oct 06, 2010 +:author: marcink +:copyright: (c) 2013 RhodeCode GmbH, and others. +:license: GPLv3, see LICENSE.md for more details. +""" + +import base64 +import datetime +import logging +import traceback +import warnings + +import decorator +import paste.auth.basic +import paste.httpexceptions +import paste.httpheaders +import webob.exc +from tg import TGController, config, render_template, request, response, session +from tg import tmpl_context as c +from tg.i18n import ugettext as _ + +import kallithea +from kallithea.lib import auth_modules, ext_json, webutils +from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware +from kallithea.lib.exceptions import UserCreationError +from kallithea.lib.utils import get_repo_slug, is_valid_repo +from kallithea.lib.utils2 import AttributeDict, asbool, ascii_bytes, safe_int, safe_str, set_hook_environment +from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError +from kallithea.lib.webutils import url +from kallithea.model import db, meta +from kallithea.model.scm import ScmModel + + +log = logging.getLogger(__name__) + + +def render(template_path): + return render_template({'url': url}, 'mako', template_path) + + +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() + log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip) + 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) + + +def get_path_info(environ): + """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. + """ + org_req = environ.get('tg.original_request') + if org_req is not None: + environ = org_req.environ + return safe_str(environ['PATH_INFO'].encode('latin1')) + + +def log_in_user(user, remember, is_external_auth, ip_addr): + """ + 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. + + Returns populated `AuthUser` object. + """ + # It should not be possible to explicitly log in as the default user. + assert not user.is_default_user, user + + auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr) + if auth_user is None: + return None + + user.update_lastlogin() + meta.Session().commit() + + # Start new session to prevent session fixation attacks. + session.invalidate() + session['authuser'] = cookie = auth_user.to_cookie() + + # If they want to be remembered, update the cookie. + # NOTE: Assumes that beaker defaults to browser session cookie. + 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 ' + 'session, session attrs %s', user.username, cookie) + + # dumps session attrs back to cookie + session._update_cookie_out() + + return auth_user + + +class BasicAuth(paste.auth.basic.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, environ): + head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) + # 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) + if self._rc_auth_http_code and self._rc_auth_http_code == '403': + # return 403 if alternative http return code is specified in + # Kallithea config + return paste.httpexceptions.HTTPForbidden(headers=head) + return paste.httpexceptions.HTTPUnauthorized(headers=head) + + def authenticate(self, environ): + authorization = paste.httpheaders.AUTHORIZATION(environ) + if not authorization: + return self.build_authentication(environ) + (authmeth, auth) = authorization.split(' ', 1) + if 'basic' != authmeth.lower(): + return self.build_authentication(environ) + auth = safe_str(base64.b64decode(auth.strip())) + _parts = auth.split(':', 1) + if len(_parts) == 2: + username, password = _parts + if self.authfunc(username, password, environ) is not None: + return username + return self.build_authentication(environ) + + __call__ = authenticate + + +class BaseVCSController(object): + """Base controller for handling Mercurial/Git protocol requests + (coming from a VCS client, and not a browser). + """ + + scm_alias = None # 'hg' / 'git' + + def __init__(self, application, config): + self.application = application + self.config = config + # base path of repo locations + self.basepath = self.config['base_path'] + # authenticate this VCS request using the authentication modules + self.authenticate = BasicAuth('', auth_modules.authenticate, + config.get('auth_ret_code')) + + @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() + + def _authorize(self, environ, action, repo_name, ip_addr): + """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. + """ + # Use anonymous access if allowed for action on repo. + default_user = db.User.get_default_user() + default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) + if default_authuser is None: + log.debug('No anonymous access at all') # move on to proper user auth + else: + if self._check_permission(action, default_authuser, repo_name): + return default_authuser, None + log.debug('Not authorized to access this repository as anonymous user') + + username = None + #============================================================== + # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE + # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS + #============================================================== + + # 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) + + # If not authenticated by the container, running basic auth + if not username: + self.authenticate.realm = self.config['realm'] + 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 + + #============================================================== + # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME + #============================================================== + try: + user = db.User.get_by_username_or_email(username) + except Exception: + log.error(traceback.format_exc()) + return None, webob.exc.HTTPInternalServerError() + + authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr) + if authuser is None: + return None, webob.exc.HTTPForbidden() + if not self._check_permission(action, authuser, repo_name): + return None, webob.exc.HTTPForbidden() + + return user, None + + def _handle_request(self, environ, start_response): + raise NotImplementedError() + + def _check_permission(self, action, authuser, repo_name): + """ + :param action: 'push' or 'pull' + :param user: `AuthUser` instance + :param repo_name: repository name + """ + if action == 'push': + if not HasPermissionAnyMiddleware('repository.write', + 'repository.admin')(authuser, + repo_name): + return False + + elif action == 'pull': + #any other action need at least read permission + if not HasPermissionAnyMiddleware('repository.read', + 'repository.write', + 'repository.admin')(authuser, + repo_name): + return False + + else: + assert False, action + + return True + + def __call__(self, environ, start_response): + try: + # try parsing a request for this VCS - if it fails, call the wrapped app + parsed_request = self.parse_request(environ) + if parsed_request is None: + return self.application(environ, start_response) + + # 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 = 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 + #====================================================================== + set_hook_environment(user.username, ip_addr, + parsed_request.repo_name, self.scm_alias, parsed_request.action) + + try: + log.info('%s action on %s repo "%s" by "%s" from %s', + parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr) + app = self._make_app(parsed_request) + return app(environ, start_response) + except Exception: + log.error(traceback.format_exc()) + raise webob.exc.HTTPInternalServerError() + + except webob.exc.HTTPException as e: + return e(environ, start_response) + + +class BaseController(TGController): + + def _before(self, *args, **kwargs): + """ + _before is called before controller methods and after __call__ + """ + 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. + token = request.POST.get(webutils.session_csrf_secret_name) + if not token or token != webutils.session_csrf_secret_token(): + log.error('CSRF check failed') + raise webob.exc.HTTPForbidden() + + c.kallithea_version = kallithea.__version__ + settings = db.Setting.get_app_settings() + + # Visual options + c.visual = AttributeDict({}) + + ## DB stored + c.visual.show_public_icon = asbool(settings.get('show_public_icon')) + c.visual.show_private_icon = asbool(settings.get('show_private_icon')) + c.visual.stylify_metalabels = asbool(settings.get('stylify_metalabels')) + c.visual.page_size = safe_int(settings.get('dashboard_items', 100)) + c.visual.admin_grid_items = safe_int(settings.get('admin_grid_items', 100)) + c.visual.repository_fields = asbool(settings.get('repository_fields')) + c.visual.show_version = asbool(settings.get('show_version')) + c.visual.use_gravatar = asbool(settings.get('use_gravatar')) + c.visual.gravatar_url = settings.get('gravatar_url') + + c.ga_code = settings.get('ga_code') + # 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 = '''''' % c.ga_code + c.site_name = settings.get('title') + c.clone_uri_tmpl = settings.get('clone_uri_tmpl') or db.Repository.DEFAULT_CLONE_URI + c.clone_ssh_tmpl = settings.get('clone_ssh_tmpl') or db.Repository.DEFAULT_CLONE_SSH + + ## INI stored + c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True)) + c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True)) + c.ssh_enabled = asbool(config.get('ssh_enabled', False)) + + c.instance_id = config.get('instance_id') + c.issues_url = config.get('bugtracker', url('issues_url')) + # END CONFIG VARS + + c.repo_name = get_repo_slug(request) # can be empty + c.backends = list(kallithea.BACKENDS) + + self.cut_off_limit = safe_int(config.get('cut_off_limit')) + + c.my_pr_count = db.PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count() + + self.scm_model = ScmModel() + + @staticmethod + def _determine_auth_user(session_authuser, ip_addr): + """ + Create an `AuthUser` object given the API key/bearer token + (if any) and the value of the authuser session cookie. + Returns None if no valid user is found (like not active or no access for IP). + """ + + # Authenticate by session cookie + # In ancient login sessions, 'authuser' may not be a dict. + # In that case, the user will have to log in again. + # 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): + return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr) + + # Authenticate by auth_container plugin (if enabled) + if any( + plugin.is_container_auth + for plugin in auth_modules.get_auth_plugins() + ): + try: + user_info = auth_modules.authenticate('', '', request.environ) + except UserCreationError as e: + webutils.flash(e, 'error', logf=log.error) + else: + if user_info is not None: + username = user_info['username'] + user = db.User.get_by_username(username, case_insensitive=True) + return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr) + + # User is default user (if active) or anonymous + default_user = db.User.get_default_user() + authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) + if authuser is None: # fall back to anonymous + authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make? + return authuser + + @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() + + # Make sure CSRF token never appears in the URL. If so, invalidate it. + if webutils.session_csrf_secret_name in request.GET: + log.error('CSRF key leak detected') + session.pop(webutils.session_csrf_secret_name, None) + session.save() + webutils.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() + + def __call__(self, environ, context): + try: + ip_addr = get_ip_addr(environ) + self._basic_security_checks() + + api_key = request.GET.get('api_key') + try: + # Request.authorization may raise ValueError on invalid input + type, params = request.authorization + except (ValueError, TypeError): + pass + else: + if type.lower() == 'bearer': + 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, + ) + needs_csrf_check = request.method not in ['GET', 'HEAD'] + + else: + dbuser = db.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) + authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr) + needs_csrf_check = False # API key provides CSRF protection + + if authuser is None: + log.info('No valid user found') + raise webob.exc.HTTPForbidden() + + # set globals for auth user + request.authuser = authuser + request.ip_addr = ip_addr + request.needs_csrf_check = needs_csrf_check + + log.info('IP: %s User: %s Request: %s', + request.ip_addr, request.authuser, + get_path_info(environ), + ) + return super(BaseController, self).__call__(environ, context) + except webob.exc.HTTPException as e: + return e + + +class BaseRepoController(BaseController): + """ + Base class for controllers responsible for loading all needed data for + repository loaded items are + + c.db_repo_scm_instance: instance of scm repository + c.db_repo: instance of db + c.repository_followers: number of followers + c.repository_forks: number of forks + c.repository_following: weather the current user is following the current repo + """ + + def _before(self, *args, **kwargs): + super(BaseRepoController, self)._before(*args, **kwargs) + if c.repo_name: # extracted from request by base-base BaseController._before + _dbr = db.Repository.get_by_repo_name(c.repo_name) + if not _dbr: + return + + log.debug('Found repository in database %s with state `%s`', + _dbr, _dbr.repo_state) + 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 [db.Repository.STATE_PENDING]: + if route in ['repo_creating_home']: + return + check_url = url('repo_creating_home', repo_name=c.repo_name) + raise webob.exc.HTTPFound(location=check_url) + + dbr = c.db_repo = _dbr + c.db_repo_scm_instance = c.db_repo.scm_instance + if c.db_repo_scm_instance is None: + log.error('%s this repository is present in database but it ' + 'cannot be created as an scm instance', c.repo_name) + webutils.flash(_('Repository not found in the filesystem'), + category='error') + raise webob.exc.HTTPNotFound() + + # 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( + c.repo_name, request.authuser.user_id) + + @staticmethod + def _get_ref_rev(repo, ref_type, ref_name, returnempty=False): + """ + Safe way to get changeset. If error occurs show error. + """ + try: + return repo.scm_instance.get_ref_revision(ref_type, ref_name) + except EmptyRepositoryError as e: + if returnempty: + return repo.scm_instance.EMPTY_CHANGESET + webutils.flash(_('There are no changesets yet'), category='error') + raise webob.exc.HTTPNotFound() + except ChangesetDoesNotExistError as e: + webutils.flash(_('Changeset for %s %s not found in %s') % + (ref_type, ref_name, repo.repo_name), + category='error') + raise webob.exc.HTTPNotFound() + except RepositoryError as e: + log.error(traceback.format_exc()) + webutils.flash(e, category='error') + raise webob.exc.HTTPBadRequest() + + +@decorator.decorator +def jsonify(func, *args, **kwargs): + """Action decorator that formats output for JSON + + 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 ''' % c.ga_code - c.site_name = settings.get('title') - c.clone_uri_tmpl = settings.get('clone_uri_tmpl') or db.Repository.DEFAULT_CLONE_URI - c.clone_ssh_tmpl = settings.get('clone_ssh_tmpl') or db.Repository.DEFAULT_CLONE_SSH - - ## INI stored - c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True)) - c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True)) - c.ssh_enabled = asbool(config.get('ssh_enabled', False)) - - c.instance_id = config.get('instance_id') - c.issues_url = config.get('bugtracker', url('issues_url')) - # END CONFIG VARS - - c.repo_name = get_repo_slug(request) # can be empty - c.backends = list(kallithea.BACKENDS) - - self.cut_off_limit = safe_int(config.get('cut_off_limit')) - - c.my_pr_count = db.PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count() - - self.scm_model = ScmModel() - - @staticmethod - def _determine_auth_user(session_authuser, ip_addr): - """ - Create an `AuthUser` object given the API key/bearer token - (if any) and the value of the authuser session cookie. - Returns None if no valid user is found (like not active or no access for IP). - """ - - # Authenticate by session cookie - # In ancient login sessions, 'authuser' may not be a dict. - # In that case, the user will have to log in again. - # 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): - return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr) - - # Authenticate by auth_container plugin (if enabled) - if any( - plugin.is_container_auth - for plugin in auth_modules.get_auth_plugins() - ): - try: - user_info = auth_modules.authenticate('', '', request.environ) - except UserCreationError as e: - webutils.flash(e, 'error', logf=log.error) - else: - if user_info is not None: - username = user_info['username'] - user = db.User.get_by_username(username, case_insensitive=True) - return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr) - - # User is default user (if active) or anonymous - default_user = db.User.get_default_user() - authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) - if authuser is None: # fall back to anonymous - authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make? - return authuser - - @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() - - # Make sure CSRF token never appears in the URL. If so, invalidate it. - if webutils.session_csrf_secret_name in request.GET: - log.error('CSRF key leak detected') - session.pop(webutils.session_csrf_secret_name, None) - session.save() - webutils.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() - - def __call__(self, environ, context): - try: - ip_addr = get_ip_addr(environ) - self._basic_security_checks() - - api_key = request.GET.get('api_key') - try: - # Request.authorization may raise ValueError on invalid input - type, params = request.authorization - except (ValueError, TypeError): - pass - else: - if type.lower() == 'bearer': - 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, - ) - needs_csrf_check = request.method not in ['GET', 'HEAD'] - - else: - dbuser = db.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) - authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr) - needs_csrf_check = False # API key provides CSRF protection - - if authuser is None: - log.info('No valid user found') - raise webob.exc.HTTPForbidden() - - # set globals for auth user - request.authuser = authuser - request.ip_addr = ip_addr - request.needs_csrf_check = needs_csrf_check - - log.info('IP: %s User: %s Request: %s', - request.ip_addr, request.authuser, - get_path_info(environ), - ) - return super(BaseController, self).__call__(environ, context) - except webob.exc.HTTPException as e: - return e - - -class BaseRepoController(BaseController): - """ - Base class for controllers responsible for loading all needed data for - repository loaded items are - - c.db_repo_scm_instance: instance of scm repository - c.db_repo: instance of db - c.repository_followers: number of followers - c.repository_forks: number of forks - c.repository_following: weather the current user is following the current repo - """ - - def _before(self, *args, **kwargs): - super(BaseRepoController, self)._before(*args, **kwargs) - if c.repo_name: # extracted from request by base-base BaseController._before - _dbr = db.Repository.get_by_repo_name(c.repo_name) - if not _dbr: - return - - log.debug('Found repository in database %s with state `%s`', - _dbr, _dbr.repo_state) - 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 [db.Repository.STATE_PENDING]: - if route in ['repo_creating_home']: - return - check_url = url('repo_creating_home', repo_name=c.repo_name) - raise webob.exc.HTTPFound(location=check_url) - - dbr = c.db_repo = _dbr - c.db_repo_scm_instance = c.db_repo.scm_instance - if c.db_repo_scm_instance is None: - log.error('%s this repository is present in database but it ' - 'cannot be created as an scm instance', c.repo_name) - webutils.flash(_('Repository not found in the filesystem'), - category='error') - raise webob.exc.HTTPNotFound() - - # 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( - c.repo_name, request.authuser.user_id) - - @staticmethod - def _get_ref_rev(repo, ref_type, ref_name, returnempty=False): - """ - Safe way to get changeset. If error occurs show error. - """ - try: - return repo.scm_instance.get_ref_revision(ref_type, ref_name) - except EmptyRepositoryError as e: - if returnempty: - return repo.scm_instance.EMPTY_CHANGESET - webutils.flash(_('There are no changesets yet'), category='error') - raise webob.exc.HTTPNotFound() - except ChangesetDoesNotExistError as e: - webutils.flash(_('Changeset for %s %s not found in %s') % - (ref_type, ref_name, repo.repo_name), - category='error') - raise webob.exc.HTTPNotFound() - except RepositoryError as e: - log.error(traceback.format_exc()) - webutils.flash(e, category='error') - raise webob.exc.HTTPBadRequest() - - -@decorator.decorator -def jsonify(func, *args, **kwargs): - """Action decorator that formats output for JSON - - 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