Mercurial > kallithea
changeset 8809:4f0de9468da3
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.
line wrap: on
line diff
--- 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``
--- 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
--- 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
--- 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:
--- 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')
--- 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,
--- 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,
--- 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()
--- 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:
--- 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')
--- 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
--- 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')
--- 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)
--- 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')
--- 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)
--- 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, [])
--- /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 <http://www.gnu.org/licenses/>. + +""" +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 = '''<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 + 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 <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") + return ascii_bytes(ext_json.dumps(data)) + +@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: + webutils.flash(_("SSH access is disabled."), category='warning') + raise webob.exc.HTTPNotFound() + return func(*args, **kwargs)
--- a/kallithea/controllers/changelog.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/changelog.py Fri Nov 13 01:06:16 2020 +0100 @@ -33,9 +33,9 @@ from tg.i18n import ugettext as _ from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound +from kallithea.controllers import base from kallithea.lib import webutils from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseRepoController, render from kallithea.lib.graphmod import graph_data from kallithea.lib.page import Page from kallithea.lib.utils2 import safe_int @@ -46,7 +46,7 @@ log = logging.getLogger(__name__) -class ChangelogController(BaseRepoController): +class ChangelogController(base.BaseRepoController): def _before(self, *args, **kwargs): super(ChangelogController, self)._before(*args, **kwargs) @@ -146,12 +146,12 @@ c.revision = revision # requested revision ref c.first_revision = c.cs_pagination[0] # pagination is never empty here! - return render('changelog/changelog.html') + return base.render('changelog/changelog.html') @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') def changelog_details(self, cs): if request.environ.get('HTTP_X_PARTIAL_XHR'): c.cs = c.db_repo_scm_instance.get_changeset(cs) - return render('changelog/changelog_details.html') + return base.render('changelog/changelog_details.html') raise HTTPNotFound()
--- a/kallithea/controllers/changeset.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/changeset.py Fri Nov 13 01:06:16 2020 +0100 @@ -36,9 +36,9 @@ from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNotFound import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.lib import auth, diffs, webutils from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseRepoController, jsonify, render from kallithea.lib.graphmod import graph_data from kallithea.lib.utils2 import ascii_str, safe_str from kallithea.lib.vcs.backends.base import EmptyChangeset @@ -148,7 +148,7 @@ c.comment = comment data.update(comment.get_dict()) data.update({'rendered_text': - render('changeset/changeset_comment_block.html')}) + base.render('changeset/changeset_comment_block.html')}) return data @@ -170,7 +170,7 @@ else: raise HTTPForbidden() -class ChangesetController(BaseRepoController): +class ChangesetController(base.BaseRepoController): def _before(self, *args, **kwargs): super(ChangesetController, self)._before(*args, **kwargs) @@ -285,19 +285,19 @@ elif method == 'patch': response.content_type = 'text/plain' c.diff = safe_str(raw_diff) - return render('changeset/patch_changeset.html') + return base.render('changeset/patch_changeset.html') elif method == 'raw': response.content_type = 'text/plain' return raw_diff elif method == 'show': if len(c.cs_ranges) == 1: - return render('changeset/changeset.html') + return base.render('changeset/changeset.html') else: c.cs_ranges_org = None c.cs_comments = {} revs = [ctx.revision for ctx in reversed(c.cs_ranges)] c.jsdata = graph_data(c.db_repo_scm_instance, revs) - return render('changeset/changeset_range.html') + return base.render('changeset/changeset_range.html') @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') @@ -321,19 +321,19 @@ @LoginRequired() @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def comment(self, repo_name, revision): return create_cs_pr_comment(repo_name, revision=revision) @LoginRequired() @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def delete_comment(self, repo_name, comment_id): return delete_cs_pr_comment(repo_name, comment_id) @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def changeset_info(self, repo_name, revision): if request.is_xhr: try: @@ -345,7 +345,7 @@ @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def changeset_children(self, repo_name, revision): if request.is_xhr: changeset = c.db_repo_scm_instance.get_changeset(revision) @@ -358,7 +358,7 @@ @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def changeset_parents(self, repo_name, revision): if request.is_xhr: changeset = c.db_repo_scm_instance.get_changeset(revision)
--- a/kallithea/controllers/compare.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/compare.py Fri Nov 13 01:06:16 2020 +0100 @@ -35,9 +35,9 @@ from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.lib import diffs, webutils from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseRepoController, render from kallithea.lib.graphmod import graph_data from kallithea.lib.webutils import url from kallithea.model import db @@ -46,7 +46,7 @@ log = logging.getLogger(__name__) -class CompareController(BaseRepoController): +class CompareController(base.BaseRepoController): def _before(self, *args, **kwargs): super(CompareController, self)._before(*args, **kwargs) @@ -76,7 +76,7 @@ def index(self, repo_name): c.compare_home = True c.a_ref_name = c.cs_ref_name = None - return render('compare/compare_diff.html') + return base.render('compare/compare_diff.html') @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') @@ -129,7 +129,7 @@ c.jsdata = graph_data(c.cs_repo.scm_instance, revs) if partial: - return render('compare/compare_cs.html') + return base.render('compare/compare_cs.html') org_repo = c.a_repo other_repo = c.cs_repo @@ -185,4 +185,4 @@ html_diff = diffs.as_html(parsed_lines=[f]) c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, html_diff, st)) - return render('compare/compare_diff.html') + return base.render('compare/compare_diff.html')
--- a/kallithea/controllers/error.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/error.py Fri Nov 13 01:06:16 2020 +0100 @@ -32,13 +32,13 @@ from tg import tmpl_context as c from tg.i18n import ugettext as _ -from kallithea.lib.base import BaseController +from kallithea.controllers import base log = logging.getLogger(__name__) -class ErrorController(BaseController): +class ErrorController(base.BaseController): """Generates error documents as and when they are required. The errorpage middleware renders /error/document when error
--- a/kallithea/controllers/feed.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/feed.py Fri Nov 13 01:06:16 2020 +0100 @@ -35,9 +35,9 @@ import kallithea import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.lib import feeds, webutils from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseRepoController from kallithea.lib.diffs import DiffProcessor from kallithea.lib.utils2 import asbool, fmt_date, safe_int, safe_str, shorter @@ -45,7 +45,7 @@ log = logging.getLogger(__name__) -class FeedController(BaseRepoController): +class FeedController(base.BaseRepoController): @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read')
--- a/kallithea/controllers/files.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/files.py Fri Nov 13 01:06:16 2020 +0100 @@ -40,9 +40,9 @@ import kallithea import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.lib import diffs, webutils from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseRepoController, jsonify, render from kallithea.lib.exceptions import NonRelativePathError from kallithea.lib.utils2 import asbool, convert_line_endings, detect_mode, safe_str from kallithea.lib.vcs.backends.base import EmptyChangeset @@ -60,7 +60,7 @@ log = logging.getLogger(__name__) -class FilesController(BaseRepoController): +class FilesController(base.BaseRepoController): def _before(self, *args, **kwargs): super(FilesController, self)._before(*args, **kwargs) @@ -179,7 +179,7 @@ raise HTTPNotFound() if request.environ.get('HTTP_X_PARTIAL_XHR'): - return render('files/files_ypjax.html') + return base.render('files/files_ypjax.html') # TODO: tags and bookmarks? c.revision_options = [(c.changeset.raw_id, @@ -190,11 +190,11 @@ c.revision_options += [('-', '-')] + \ [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()] - return render('files/files.html') + return base.render('files/files.html') @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def history(self, repo_name, revision, f_path): changeset = self.__get_cs(revision) _file = changeset.get_node(f_path) @@ -224,7 +224,7 @@ c.authors = [] for a in set([x.author for x in _hist]): c.authors.append((author_email(a), h.person(a))) - return render('files/files_history_box.html') + return base.render('files/files_history_box.html') @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') @@ -335,7 +335,7 @@ raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, revision='tip')) - return render('files/files_delete.html') + return base.render('files/files_delete.html') @LoginRequired() @HasRepoPermissionLevelDecorator('write') @@ -393,7 +393,7 @@ raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, revision='tip')) - return render('files/files_edit.html') + return base.render('files/files_edit.html') @LoginRequired() @HasRepoPermissionLevelDecorator('write') @@ -468,7 +468,7 @@ raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, revision='tip')) - return render('files/files_add.html') + return base.render('files/files_add.html') @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') @@ -645,7 +645,7 @@ ignore_whitespace=ignore_whitespace_diff, line_context=diff_context_size) c.file_diff_data = [(fid, fid, op, a_path, node2.path, diff, st)] - return render('files/file_diff.html') + return base.render('files/file_diff.html') @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') @@ -693,7 +693,7 @@ c.cs1 = c.changeset_1 c.cs2 = c.changeset_2 - return render('files/diff_2way.html') + return base.render('files/diff_2way.html') def _get_node_history(self, cs, f_path, changesets=None): """ @@ -736,7 +736,7 @@ @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def nodelist(self, repo_name, revision, f_path): if request.environ.get('HTTP_X_PARTIAL_XHR'): cs = self.__get_cs(revision)
--- a/kallithea/controllers/followers.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/followers.py Fri Nov 13 01:06:16 2020 +0100 @@ -30,8 +30,8 @@ from tg import request from tg import tmpl_context as c +from kallithea.controllers import base from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseRepoController, render from kallithea.lib.page import Page from kallithea.lib.utils2 import safe_int from kallithea.model import db @@ -40,7 +40,7 @@ log = logging.getLogger(__name__) -class FollowersController(BaseRepoController): +class FollowersController(base.BaseRepoController): @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') @@ -52,6 +52,6 @@ c.followers_pager = Page(d, page=p, items_per_page=20) if request.environ.get('HTTP_X_PARTIAL_XHR'): - return render('/followers/followers_data.html') + return base.render('/followers/followers_data.html') - return render('/followers/followers.html') + return base.render('/followers/followers.html')
--- a/kallithea/controllers/forks.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/forks.py Fri Nov 13 01:06:16 2020 +0100 @@ -36,9 +36,9 @@ from webob.exc import HTTPFound, HTTPNotFound import kallithea +from kallithea.controllers import base from kallithea.lib import webutils from kallithea.lib.auth import HasPermissionAnyDecorator, HasRepoPermissionLevel, HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseRepoController, render from kallithea.lib.page import Page from kallithea.lib.utils2 import safe_int from kallithea.model import db @@ -50,7 +50,7 @@ log = logging.getLogger(__name__) -class ForksController(BaseRepoController): +class ForksController(base.BaseRepoController): def __load_defaults(self): c.repo_groups = AvailableRepoGroupChoices('write') @@ -113,9 +113,9 @@ c.forks_pager = Page(d, page=p, items_per_page=20) if request.environ.get('HTTP_X_PARTIAL_XHR'): - return render('/forks/forks_data.html') + return base.render('/forks/forks_data.html') - return render('/forks/forks.html') + return base.render('/forks/forks.html') @LoginRequired() @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') @@ -128,7 +128,7 @@ defaults = self.__load_data() return htmlfill.render( - render('forks/fork.html'), + base.render('forks/fork.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @@ -157,7 +157,7 @@ task_id = task.task_id except formencode.Invalid as errors: return htmlfill.render( - render('forks/fork.html'), + base.render('forks/fork.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False,
--- a/kallithea/controllers/home.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/home.py Fri Nov 13 01:06:16 2020 +0100 @@ -35,8 +35,8 @@ from webob.exc import HTTPBadRequest import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseController, jsonify, render from kallithea.lib.utils2 import safe_str from kallithea.model import db from kallithea.model.repo import RepoModel @@ -46,10 +46,10 @@ log = logging.getLogger(__name__) -class HomeController(BaseController): +class HomeController(base.BaseController): def about(self): - return render('/about.html') + return base.render('/about.html') @LoginRequired(allow_default_user=True) def index(self): @@ -62,10 +62,10 @@ repo_groups_list=repo_groups_list, short_name=True) - return render('/index.html') + return base.render('/index.html') @LoginRequired(allow_default_user=True) - @jsonify + @base.jsonify def repo_switcher_data(self): if request.is_xhr: all_repos = db.Repository.query(sorted=True).all() @@ -109,7 +109,7 @@ @LoginRequired(allow_default_user=True) @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def repo_refs_data(self, repo_name): repo = db.Repository.get_by_repo_name(repo_name).scm_instance res = [] @@ -144,7 +144,7 @@ return data @LoginRequired() - @jsonify + @base.jsonify def users_and_groups_data(self): """ Returns 'results' with a list of users and user groups.
--- a/kallithea/controllers/journal.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/journal.py Fri Nov 13 01:06:16 2020 +0100 @@ -37,10 +37,10 @@ from webob.exc import HTTPBadRequest import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.controllers.admin.admin import _journal_filter from kallithea.lib import feeds, webutils from kallithea.lib.auth import LoginRequired -from kallithea.lib.base import BaseController, render from kallithea.lib.page import Page from kallithea.lib.utils2 import AttributeDict, safe_int from kallithea.model import db, meta @@ -55,7 +55,7 @@ feed_nr = 20 -class JournalController(BaseController): +class JournalController(base.BaseController): def _before(self, *args, **kwargs): super(JournalController, self)._before(*args, **kwargs) @@ -178,7 +178,7 @@ c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager) if request.environ.get('HTTP_X_PARTIAL_XHR'): - return render('journal/journal_data.html') + return base.render('journal/journal_data.html') repos_list = db.Repository.query(sorted=True) \ .filter_by(owner_id=request.authuser.user_id).all() @@ -187,7 +187,7 @@ # data used to render the grid c.data = repos_data - return render('journal/journal.html') + return base.render('journal/journal.html') @LoginRequired() def journal_atom(self): @@ -250,9 +250,9 @@ c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager) if request.environ.get('HTTP_X_PARTIAL_XHR'): - return render('journal/journal_data.html') + return base.render('journal/journal_data.html') - return render('journal/public_journal.html') + return base.render('journal/public_journal.html') @LoginRequired(allow_default_user=True) def public_journal_atom(self):
--- a/kallithea/controllers/login.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/login.py Fri Nov 13 01:06:16 2020 +0100 @@ -36,9 +36,9 @@ from tg.i18n import ugettext as _ from webob.exc import HTTPBadRequest, HTTPFound +from kallithea.controllers import base from kallithea.lib import webutils from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator -from kallithea.lib.base import BaseController, log_in_user, render from kallithea.lib.exceptions import UserCreationError from kallithea.lib.recaptcha import submit from kallithea.lib.webutils import url @@ -50,7 +50,7 @@ log = logging.getLogger(__name__) -class LoginController(BaseController): +class LoginController(base.BaseController): def _validate_came_from(self, came_from, _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")): @@ -89,7 +89,7 @@ # remove password from filling in form again defaults.pop('password', None) return htmlfill.render( - render('/login.html'), + base.render('/login.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -103,7 +103,7 @@ webutils.flash(e, 'error') else: # login_form already validated the password - now set the session cookie accordingly - auth_user = log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr) + auth_user = base.log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr) if auth_user: raise HTTPFound(location=c.came_from) webutils.flash(_('Authentication failed.'), 'error') @@ -113,7 +113,7 @@ raise HTTPFound(location=c.came_from) # continue to show login to default user - return render('/login.html') + return base.render('/login.html') @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate') @@ -151,7 +151,7 @@ except formencode.Invalid as errors: return htmlfill.render( - render('/register.html'), + base.render('/register.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -164,7 +164,7 @@ # Exception itself webutils.flash(e, 'error') - return render('/register.html') + return base.render('/register.html') def password_reset(self): settings = db.Setting.get_app_settings() @@ -193,14 +193,14 @@ except formencode.Invalid as errors: return htmlfill.render( - render('/password_reset.html'), + base.render('/password_reset.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, encoding="UTF-8", force_defaults=False) - return render('/password_reset.html') + return base.render('/password_reset.html') def password_reset_confirmation(self): # This controller handles both GET and POST requests, though we @@ -213,14 +213,14 @@ c.timestamp = request.params.get('timestamp') or '' c.token = request.params.get('token') or '' if not request.POST: - return render('/password_reset_confirmation.html') + return base.render('/password_reset_confirmation.html') form = PasswordResetConfirmationForm()() try: form_result = form.to_python(dict(request.POST)) except formencode.Invalid as errors: return htmlfill.render( - render('/password_reset_confirmation.html'), + base.render('/password_reset_confirmation.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, @@ -232,7 +232,7 @@ form_result['token'], ): return htmlfill.render( - render('/password_reset_confirmation.html'), + base.render('/password_reset_confirmation.html'), defaults=form_result, errors={'token': _('Invalid password reset token')}, prefix_error=False,
--- a/kallithea/controllers/pullrequests.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/pullrequests.py Fri Nov 13 01:06:16 2020 +0100 @@ -36,10 +36,10 @@ from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound import kallithea.lib.helpers as h +from kallithea.controllers import base from kallithea.controllers.changeset import create_cs_pr_comment, delete_cs_pr_comment from kallithea.lib import auth, diffs, webutils from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseRepoController, jsonify, render from kallithea.lib.graphmod import graph_data from kallithea.lib.page import Page from kallithea.lib.utils2 import ascii_bytes, safe_bytes, safe_int @@ -69,7 +69,7 @@ return user -class PullrequestsController(BaseRepoController): +class PullrequestsController(base.BaseRepoController): def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None): """return a structure with scm repo's interesting changesets, suitable for @@ -211,7 +211,7 @@ c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100, **url_params) - return render('/pullrequests/pullrequest_show_all.html') + return base.render('/pullrequests/pullrequest_show_all.html') @LoginRequired() def show_my(self): @@ -236,7 +236,7 @@ else: c.participate_in_pull_requests_todo.append(pr) - return render('/pullrequests/pullrequest_show_my.html') + return base.render('/pullrequests/pullrequest_show_my.html') @LoginRequired() @HasRepoPermissionLevelDecorator('read') @@ -291,11 +291,11 @@ for fork in org_repo.forks: c.a_repos.append((fork.repo_name, fork.repo_name)) - return render('/pullrequests/pullrequest.html') + return base.render('/pullrequests/pullrequest.html') @LoginRequired() @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def repo_info(self, repo_name): repo = c.db_repo refs, selected_ref = self._get_repo_refs(repo.scm_instance) @@ -432,7 +432,7 @@ @LoginRequired() @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def delete(self, repo_name, pull_request_id): pull_request = db.PullRequest.get_or_404(pull_request_id) # only owner can delete it ! @@ -620,11 +620,11 @@ c.is_ajax_preview = False c.ancestors = None # [c.a_rev] ... but that is shown in an other way - return render('/pullrequests/pullrequest_show.html') + return base.render('/pullrequests/pullrequest_show.html') @LoginRequired() @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def comment(self, repo_name, pull_request_id): pull_request = db.PullRequest.get_or_404(pull_request_id) allowed_to_change_status = self._is_allowed_to_change_status(pull_request) @@ -633,6 +633,6 @@ @LoginRequired() @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def delete_comment(self, repo_name, comment_id): return delete_cs_pr_comment(repo_name, comment_id)
--- a/kallithea/controllers/root.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/root.py Fri Nov 13 01:06:16 2020 +0100 @@ -14,9 +14,9 @@ from tg import config from tgext.routes import RoutedController +from kallithea.controllers import base from kallithea.controllers.error import ErrorController from kallithea.controllers.routing import make_map -from kallithea.lib.base import BaseController # This is the main Kallithea entry point; TurboGears will forward all requests @@ -26,7 +26,7 @@ # The mapper is configured using routes defined in routing.py. This use of the # 'mapper' attribute is a feature of tgext.routes, which is activated by # inheriting from its RoutedController class. -class RootController(RoutedController, BaseController): +class RootController(RoutedController, base.BaseController): def __init__(self): self.mapper = make_map(config)
--- a/kallithea/controllers/search.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/search.py Fri Nov 13 01:06:16 2020 +0100 @@ -35,8 +35,8 @@ from whoosh.qparser import QueryParser, QueryParserError from whoosh.query import Phrase, Prefix +from kallithea.controllers import base from kallithea.lib.auth import LoginRequired -from kallithea.lib.base import BaseRepoController, render from kallithea.lib.indexers import CHGSET_IDX_NAME, CHGSETS_SCHEMA, IDX_NAME, SCHEMA, WhooshResultWrapper from kallithea.lib.page import Page from kallithea.lib.utils2 import safe_int @@ -46,7 +46,7 @@ log = logging.getLogger(__name__) -class SearchController(BaseRepoController): +class SearchController(base.BaseRepoController): @LoginRequired(allow_default_user=True) def index(self, repo_name=None): @@ -139,4 +139,4 @@ c.runtime = _('An error occurred during search operation.') # Return a rendered template - return render('/search/search.html') + return base.render('/search/search.html')
--- a/kallithea/controllers/summary.py Mon Dec 14 23:00:25 2020 +0100 +++ b/kallithea/controllers/summary.py Fri Nov 13 01:06:16 2020 +0100 @@ -38,9 +38,9 @@ from tg.i18n import ugettext as _ from webob.exc import HTTPBadRequest +from kallithea.controllers import base from kallithea.lib import ext_json, webutils from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired -from kallithea.lib.base import BaseRepoController, jsonify, render from kallithea.lib.conf import ALL_EXTS, ALL_READMES, LANGUAGES_EXTENSIONS_MAP from kallithea.lib.markup_renderer import MarkupRenderer from kallithea.lib.page import Page @@ -58,7 +58,7 @@ key=lambda y:y[0][1] + y[1][1])] -class SummaryController(BaseRepoController): +class SummaryController(base.BaseRepoController): def __get_readme_data(self, db_repo): repo_name = db_repo.repo_name @@ -148,11 +148,11 @@ c.enable_downloads = c.db_repo.enable_downloads c.readme_data, c.readme_file = \ self.__get_readme_data(c.db_repo) - return render('summary/summary.html') + return base.render('summary/summary.html') @LoginRequired() @HasRepoPermissionLevelDecorator('read') - @jsonify + @base.jsonify def repo_size(self, repo_name): if request.is_xhr: return c.db_repo._repo_size() @@ -209,4 +209,4 @@ recurse_limit = 500 # don't recurse more than 500 times when parsing async_tasks.get_commits_stats(c.db_repo.repo_name, ts_min_y, ts_max_y, recurse_limit) - return render('summary/statistics.html') + return base.render('summary/statistics.html')
--- a/kallithea/lib/base.py Mon Dec 14 23:00:25 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,639 +0,0 @@ -# -*- 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 - -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 = '''<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 - 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 <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") - return ascii_bytes(ext_json.dumps(data)) - -@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: - webutils.flash(_("SSH access is disabled."), category='warning') - raise webob.exc.HTTPNotFound() - return func(*args, **kwargs)
--- a/scripts/deps.py Mon Dec 14 23:00:25 2020 +0100 +++ b/scripts/deps.py Fri Nov 13 01:06:16 2020 +0100 @@ -131,10 +131,10 @@ normal_modules = set(''' kallithea +kallithea.controllers.base kallithea.lib kallithea.lib.auth kallithea.lib.auth_modules -kallithea.lib.base kallithea.lib.celerylib kallithea.lib.db_manage kallithea.lib.helpers