From ec1b6d985c4ca0f94145075d6b946c74495de54e Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Thu, 22 Feb 2024 13:11:18 +0100 Subject: [PATCH 001/150] chore(dev): print dev help cmd for nicer startup --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ac29ce52..3d59ea61 100644 --- a/Makefile +++ b/Makefile @@ -109,7 +109,8 @@ dev-sh: sudo apt-get install -y zsh carapace-bin rm -rf /home/rhodecode/.oh-my-zsh curl https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh | sh - echo "source <(carapace _carapace)" > /home/rhodecode/.zsrc + @echo "source <(carapace _carapace)" > /home/rhodecode/.zsrc + @echo "${RC_DEV_CMD_HELP}" PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# ' zsh From 41983731e4daa05d418cceaacd8cacff68be4a70 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 26 Feb 2024 15:02:27 +0100 Subject: [PATCH 002/150] chore(deps): bumped tzdata==2024.1 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9b32150e..e2e5c6a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ celery==5.3.6 vine==5.1.0 python-dateutil==2.8.2 six==1.16.0 - tzdata==2023.4 + tzdata==2024.1 vine==5.1.0 channelstream==0.7.1 gevent==24.2.1 @@ -275,7 +275,7 @@ sqlalchemy==1.4.51 supervisor==4.2.5 tzlocal==4.3 pytz-deprecation-shim==0.1.0.post0 - tzdata==2023.4 + tzdata==2024.1 unidecode==1.3.6 urlobject==2.4.3 waitress==3.0.0 From ca4a10b1d455e48235d0619a1505142bad36de78 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 26 Feb 2024 15:03:39 +0100 Subject: [PATCH 003/150] chore(deps): bumped zope.interface==6.2.0 --- requirements.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index e2e5c6a0..d2de6b7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ channelstream==0.7.1 gevent==24.2.1 greenlet==3.0.3 zope.event==5.0.0 - zope.interface==6.1.0 + zope.interface==6.2.0 itsdangerous==1.1.0 marshmallow==2.18.0 pyramid==2.0.2 @@ -46,7 +46,7 @@ channelstream==0.7.1 venusian==3.0.0 webob==1.8.7 zope.deprecation==5.0.0 - zope.interface==6.1.0 + zope.interface==6.2.0 pyramid-apispec==0.3.3 apispec==1.3.3 pyramid-jinja2==2.10 @@ -63,7 +63,7 @@ channelstream==0.7.1 venusian==3.0.0 webob==1.8.7 zope.deprecation==5.0.0 - zope.interface==6.1.0 + zope.interface==6.2.0 zope.deprecation==5.0.0 python-dateutil==2.8.2 six==1.16.0 @@ -95,7 +95,7 @@ gunicorn==21.2.0 gevent==24.2.1 greenlet==3.0.3 zope.event==5.0.0 - zope.interface==6.1.0 + zope.interface==6.2.0 ipython==8.14.0 backcall==0.2.0 decorator==5.1.1 @@ -210,7 +210,7 @@ pyramid-debugtoolbar==4.11 venusian==3.0.0 webob==1.8.7 zope.deprecation==5.0.0 - zope.interface==6.1.0 + zope.interface==6.2.0 pyramid-mako==1.1.0 mako==1.2.4 markupsafe==2.1.2 @@ -224,7 +224,7 @@ pyramid-debugtoolbar==4.11 venusian==3.0.0 webob==1.8.7 zope.deprecation==5.0.0 - zope.interface==6.1.0 + zope.interface==6.2.0 pyramid-mailer==0.15.1 pyramid==2.0.2 hupper==1.12 @@ -236,13 +236,13 @@ pyramid-mailer==0.15.1 venusian==3.0.0 webob==1.8.7 zope.deprecation==5.0.0 - zope.interface==6.1.0 + zope.interface==6.2.0 repoze.sendmail==4.4.1 transaction==3.1.0 - zope.interface==6.1.0 - zope.interface==6.1.0 + zope.interface==6.2.0 + zope.interface==6.2.0 transaction==3.1.0 - zope.interface==6.1.0 + zope.interface==6.2.0 python-ldap==3.4.3 pyasn1==0.4.8 pyasn1-modules==0.2.8 From 1ea821d801e7a27f01f7f93cb3e6ea1cf9a47e7a Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 26 Feb 2024 15:04:20 +0100 Subject: [PATCH 004/150] chore(deps): bumped webhelpers2==2.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d2de6b7f..24aaf3a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -285,7 +285,7 @@ weberror==0.13.1 pygments==2.15.1 tempita==0.5.2 webob==1.8.7 -webhelpers2==2.0 +webhelpers2==2.1 markupsafe==2.1.2 six==1.16.0 whoosh==2.7.4 From 6f000325684bf7c66355321bdcd7af7957fd2b73 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 26 Feb 2024 15:04:53 +0100 Subject: [PATCH 005/150] chore(deps): bumped simplejson==3.19.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 24aaf3a7..dcb2028c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -262,7 +262,7 @@ regex==2022.10.31 routes==2.5.1 repoze.lru==0.7 six==1.16.0 -simplejson==3.19.1 +simplejson==3.19.2 sshpubkeys==3.3.1 cryptography==40.0.2 cffi==1.16.0 From a7987e5b31d738b2f06f3e4ebd8f9eab950c7bc4 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 26 Feb 2024 15:05:18 +0100 Subject: [PATCH 006/150] chore(deps): bumped pycurl==7.45.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dcb2028c..279375d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -194,7 +194,7 @@ pycmarkgfm==1.2.0 cffi==1.16.0 pycparser==2.21 pycryptodome==3.17 -pycurl==7.45.2 +pycurl==7.45.3 pymysql==1.0.3 pyotp==2.8.0 pyparsing==3.1.1 From 2cfcfbda10aebb22f26cc4faeda078dcceb787a1 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 26 Feb 2024 15:06:20 +0100 Subject: [PATCH 007/150] chore(deps): bumped orjson==3.9.15 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 279375d6..eecf0875 100644 --- a/requirements.txt +++ b/requirements.txt @@ -171,7 +171,7 @@ nbconvert==7.7.3 tinycss2==1.2.1 webencodings==0.5.1 traitlets==5.9.0 -orjson==3.9.13 +orjson==3.9.15 pastescript==3.4.0 paste==3.7.1 six==1.16.0 From 4e9e770ea848b3ea27c055db84e5b626fea9cc46 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 26 Feb 2024 15:07:25 +0100 Subject: [PATCH 008/150] chore(release): bumped version to 5.1.0 --- .bumpversion.cfg | 2 +- rhodecode/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ca71cca8..c76f1bd2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 5.0.0 +current_version = 5.1.0 message = release: Bump version {current_version} to {new_version} [bumpversion:file:rhodecode/VERSION] diff --git a/rhodecode/VERSION b/rhodecode/VERSION index 28cbf7c0..acf69b48 100644 --- a/rhodecode/VERSION +++ b/rhodecode/VERSION @@ -1 +1 @@ -5.0.0 \ No newline at end of file +5.1.0 \ No newline at end of file From 17631cb9fd15d5e58abcc82b9a0d9db958a0fe5d Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 26 Feb 2024 15:19:54 +0100 Subject: [PATCH 009/150] chore(deps): bumped dogpile.cache==1.3.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eecf0875..55b03477 100644 --- a/requirements.txt +++ b/requirements.txt @@ -84,7 +84,7 @@ deform==2.0.15 zope.deprecation==5.0.0 diskcache==5.6.3 docutils==0.19 -dogpile.cache==1.3.0 +dogpile.cache==1.3.2 decorator==5.1.1 stevedore==5.1.0 pbr==5.11.1 From 16c169bec2cf50587662ef01d8e7a5936ef192eb Mon Sep 17 00:00:00 2001 From: Serhii Ilin Date: Mon, 19 Feb 2024 13:49:51 +0200 Subject: [PATCH 010/150] fix(ssh): Added alternative SshWrapper and changes needed to support it + service api. Fixes: RCCE-6 --- configs/development.ini | 6 + configs/production.ini | 6 + rhodecode/api/__init__.py | 79 ++++++----- rhodecode/api/tests/test_service_api.py | 55 ++++++++ rhodecode/api/views/service_api.py | 125 +++++++++++++++++ .../apps/ssh_support/lib/backends/__init__.py | 129 ++++++++++++++++++ .../apps/ssh_support/lib/backends/base.py | 33 +++-- rhodecode/apps/ssh_support/lib/backends/hg.py | 9 ++ .../lib/{ssh_wrapper.py => ssh_wrapper_v1.py} | 0 .../apps/ssh_support/lib/ssh_wrapper_v2.py | 72 ++++++++++ rhodecode/apps/ssh_support/tests/conftest.py | 2 +- rhodecode/lib/utils.py | 28 ++++ rhodecode/lib/vcs/exceptions.py | 4 + .../tests/fixture_mods/fixture_pyramid.py | 1 + setup.py | 3 +- 15 files changed, 505 insertions(+), 47 deletions(-) create mode 100644 rhodecode/api/tests/test_service_api.py create mode 100644 rhodecode/api/views/service_api.py rename rhodecode/apps/ssh_support/lib/{ssh_wrapper.py => ssh_wrapper_v1.py} (100%) create mode 100644 rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py diff --git a/configs/development.ini b/configs/development.ini index feed95be..d7a492a2 100644 --- a/configs/development.ini +++ b/configs/development.ini @@ -153,6 +153,12 @@ startup.import_repos = false ; SSH calls. Set this for events to receive proper url for SSH calls. app.base_url = http://rhodecode.local +; Host at which the Service API is running. +app.service_api.host = http://rhodecode.local:10020 + +; Secret for Service API authentication. +app.service_api.token = + ; Unique application ID. Should be a random unique string for security. app_instance_uuid = rc-production diff --git a/configs/production.ini b/configs/production.ini index f9c0708c..6ffeeb7d 100644 --- a/configs/production.ini +++ b/configs/production.ini @@ -104,6 +104,12 @@ startup.import_repos = false ; SSH calls. Set this for events to receive proper url for SSH calls. app.base_url = http://rhodecode.local +; Host at which the Service API is running. +app.service_api.host= http://rhodecode.local:10020 + +; Secret for Service API authentication. +app.service_api.token = + ; Unique application ID. Should be a random unique string for security. app_instance_uuid = rc-production diff --git a/rhodecode/api/__init__.py b/rhodecode/api/__init__.py index 106ff435..9a2f63e5 100644 --- a/rhodecode/api/__init__.py +++ b/rhodecode/api/__init__.py @@ -46,6 +46,7 @@ log = logging.getLogger(__name__) DEFAULT_RENDERER = 'jsonrpc_renderer' DEFAULT_URL = '/_admin/apiv2' +SERVICE_API_IDENTIFIER = 'service_' def find_methods(jsonrpc_methods, pattern): @@ -54,7 +55,9 @@ def find_methods(jsonrpc_methods, pattern): pattern = [pattern] for single_pattern in pattern: - for method_name, method in jsonrpc_methods.items(): + for method_name, method in filter( + lambda x: not x[0].startswith(SERVICE_API_IDENTIFIER), jsonrpc_methods.items() + ): if fnmatch.fnmatch(method_name, single_pattern): matches[method_name] = method return matches @@ -190,43 +193,48 @@ def request_view(request): # check if we can find this session using api_key, get_by_auth_token # search not expired tokens only try: - api_user = User.get_by_auth_token(request.rpc_api_key) + if not request.rpc_method.startswith(SERVICE_API_IDENTIFIER): + api_user = User.get_by_auth_token(request.rpc_api_key) - if api_user is None: - return jsonrpc_error( - request, retid=request.rpc_id, message='Invalid API KEY') + if api_user is None: + return jsonrpc_error( + request, retid=request.rpc_id, message='Invalid API KEY') - if not api_user.active: - return jsonrpc_error( - request, retid=request.rpc_id, - message='Request from this user not allowed') + if not api_user.active: + return jsonrpc_error( + request, retid=request.rpc_id, + message='Request from this user not allowed') - # check if we are allowed to use this IP - auth_u = AuthUser( - api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr) - if not auth_u.ip_allowed: - return jsonrpc_error( - request, retid=request.rpc_id, - message='Request from IP:{} not allowed'.format( - request.rpc_ip_addr)) + # check if we are allowed to use this IP + auth_u = AuthUser( + api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr) + if not auth_u.ip_allowed: + return jsonrpc_error( + request, retid=request.rpc_id, + message='Request from IP:{} not allowed'.format( + request.rpc_ip_addr)) + else: + log.info('Access for IP:%s allowed', request.rpc_ip_addr) + + # register our auth-user + request.rpc_user = auth_u + request.environ['rc_auth_user_id'] = str(auth_u.user_id) + + # now check if token is valid for API + auth_token = request.rpc_api_key + token_match = api_user.authenticate_by_token( + auth_token, roles=[UserApiKeys.ROLE_API]) + invalid_token = not token_match + + log.debug('Checking if API KEY is valid with proper role') + if invalid_token: + return jsonrpc_error( + request, retid=request.rpc_id, + message='API KEY invalid or, has bad role for an API call') else: - log.info('Access for IP:%s allowed', request.rpc_ip_addr) - - # register our auth-user - request.rpc_user = auth_u - request.environ['rc_auth_user_id'] = str(auth_u.user_id) - - # now check if token is valid for API - auth_token = request.rpc_api_key - token_match = api_user.authenticate_by_token( - auth_token, roles=[UserApiKeys.ROLE_API]) - invalid_token = not token_match - - log.debug('Checking if API KEY is valid with proper role') - if invalid_token: - return jsonrpc_error( - request, retid=request.rpc_id, - message='API KEY invalid or, has bad role for an API call') + auth_u = 'service' + if request.rpc_api_key != request.registry.settings['app.service_api.token']: + raise Exception("Provided service secret is not recognized!") except Exception: log.exception('Error on API AUTH') @@ -290,7 +298,8 @@ def request_view(request): }) # register some common functions for usage - attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id) + rpc_user = request.rpc_user.user_id if hasattr(request, 'rpc_user') else None + attach_context_attributes(TemplateArgs(), request, rpc_user) statsd = request.registry.statsd diff --git a/rhodecode/api/tests/test_service_api.py b/rhodecode/api/tests/test_service_api.py new file mode 100644 index 00000000..b5780777 --- /dev/null +++ b/rhodecode/api/tests/test_service_api.py @@ -0,0 +1,55 @@ + +# Copyright (C) 2010-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import pytest + +from rhodecode.api.tests.utils import ( + build_data, api_call) + + +@pytest.mark.usefixtures("app") +class TestServiceApi: + + def test_service_api_with_wrong_secret(self): + id, payload = build_data("wrong_api_key", 'service_get_repo_name_by_id') + response = api_call(self.app, payload) + + assert 'Invalid API KEY' == response.json['error'] + + def test_service_api_with_legit_secret(self): + id, payload = build_data(self.app.app.config.get_settings()['app.service_api.token'], + 'service_get_repo_name_by_id', repo_id='1') + response = api_call(self.app, payload) + assert not response.json['error'] + + def test_service_api_not_a_part_of_public_api_suggestions(self): + id, payload = build_data("secret", 'some_random_guess_method') + response = api_call(self.app, payload) + assert 'service_' not in response.json['error'] + + def test_service_get_data_for_ssh_wrapper_output(self): + id, payload = build_data( + self.app.app.config.get_settings()['app.service_api.token'], + 'service_get_data_for_ssh_wrapper', + user_id=1, + repo_name='vcs_test_git') + response = api_call(self.app, payload) + + assert ['branch_permissions', 'repo_permissions', 'repos_path', 'user_id', 'username']\ + == list(response.json['result'].keys()) diff --git a/rhodecode/api/views/service_api.py b/rhodecode/api/views/service_api.py new file mode 100644 index 00000000..e2bfa45a --- /dev/null +++ b/rhodecode/api/views/service_api.py @@ -0,0 +1,125 @@ +# Copyright (C) 2011-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging +import datetime +from collections import defaultdict + +from sqlalchemy import Table +from rhodecode.api import jsonrpc_method, SERVICE_API_IDENTIFIER + + +log = logging.getLogger(__name__) + + +@jsonrpc_method() +def service_get_data_for_ssh_wrapper(request, apiuser, user_id, repo_name, key_id=None): + from rhodecode.model.db import User + from rhodecode.model.scm import ScmModel + from rhodecode.model.meta import raw_query_executor, Base + + if key_id: + table = Table('user_ssh_keys', Base.metadata, autoload=False) + atime = datetime.datetime.utcnow() + stmt = ( + table.update() + .where(table.c.ssh_key_id == key_id) + .values(accessed_on=atime) + ) + + res_count = None + with raw_query_executor() as session: + result = session.execute(stmt) + if result.rowcount: + res_count = result.rowcount + + if res_count: + log.debug(f'Update key id:{key_id} access time') + db_user = User.get(user_id) + if not db_user: + return None + auth_user = db_user.AuthUser() + + return { + 'user_id': db_user.user_id, + 'username': db_user.username, + 'repo_permissions': auth_user.permissions['repositories'], + "branch_permissions": auth_user.get_branch_permissions(repo_name), + "repos_path": ScmModel().repos_path + } + + +@jsonrpc_method() +def service_get_repo_name_by_id(request, apiuser, repo_id): + from rhodecode.model.repo import RepoModel + by_id_match = RepoModel().get_repo_by_id(repo_id) + if by_id_match: + repo_name = by_id_match.repo_name + return { + 'repo_name': repo_name + } + return None + + +@jsonrpc_method() +def service_mark_for_invalidation(request, apiuser, repo_name): + from rhodecode.model.scm import ScmModel + ScmModel().mark_for_invalidation(repo_name) + return {'msg': "Applied"} + + +@jsonrpc_method() +def service_config_to_hgrc(request, apiuser, cli_flags, repo_name): + from rhodecode.model.db import RhodeCodeUi + from rhodecode.model.settings import VcsSettingsModel + + ui_sections = defaultdict(list) + ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None) + + default_hooks = [ + ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'), + ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'), + ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'), + + ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'), + ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'), + ] + + for k, v in default_hooks: + ui_sections['hooks'].append((k, v)) + + for entry in ui: + if not entry.active: + continue + sec = entry.section + key = entry.key + + if sec in cli_flags: + # we want only custom hooks, so we skip builtins + if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN: + continue + + ui_sections[sec].append([key, entry.value]) + + flags = [] + for _sec, key_val in ui_sections.items(): + flags.append(' ') + flags.append(f'[{_sec}]') + for key, val in key_val: + flags.append(f'{key}= {val}') + return {'flags': flags} diff --git a/rhodecode/apps/ssh_support/lib/backends/__init__.py b/rhodecode/apps/ssh_support/lib/backends/__init__.py index 8b1bfc76..d4479be6 100644 --- a/rhodecode/apps/ssh_support/lib/backends/__init__.py +++ b/rhodecode/apps/ssh_support/lib/backends/__init__.py @@ -23,6 +23,7 @@ import datetime import configparser from sqlalchemy import Table +from rhodecode.lib.utils import call_service_api from rhodecode.lib.utils2 import AttributeDict from rhodecode.model.scm import ScmModel @@ -261,3 +262,131 @@ class SshWrapper(object): exit_code = -1 return exit_code + + +class SshWrapperStandalone(SshWrapper): + """ + New version of SshWrapper designed to be depended only on service API + """ + repos_path = None + + @staticmethod + def parse_user_related_data(user_data): + user = AttributeDict() + user.user_id = user_data['user_id'] + user.username = user_data['username'] + user.repo_permissions = user_data['repo_permissions'] + user.branch_permissions = user_data['branch_permissions'] + return user + + def wrap(self): + mode = self.mode + username = self.username + user_id = self.user_id + shell = self.shell + + scm_detected, scm_repo, scm_mode = self.get_repo_details(mode) + + log.debug( + 'Mode: `%s` User: `name:%s : id:%s` Shell: `%s` SSH Command: `\"%s\"` ' + 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`', + mode, username, user_id, shell, self.command, + scm_detected, scm_mode, scm_repo) + + log.debug('SSH Connection info %s', self.get_connection_info()) + + if shell and self.command is None: + log.info('Dropping to shell, no command given and shell is allowed') + os.execl('/bin/bash', '-l') + exit_code = 1 + + elif scm_detected: + data = call_service_api(self.ini_path, { + "method": "service_get_data_for_ssh_wrapper", + "args": {"user_id": user_id, "repo_name": scm_repo, "key_id": self.key_id} + }) + user = self.parse_user_related_data(data) + if not user: + log.warning('User with id %s not found', user_id) + exit_code = -1 + return exit_code + self.repos_path = data['repos_path'] + permissions = user.repo_permissions + repo_branch_permissions = user.branch_permissions + try: + exit_code, is_updated = self.serve( + scm_detected, scm_repo, scm_mode, user, permissions, + repo_branch_permissions) + except Exception: + log.exception('Error occurred during execution of SshWrapper') + exit_code = -1 + + elif self.command is None and shell is False: + log.error('No Command given.') + exit_code = -1 + + else: + log.error('Unhandled Command: "%s" Aborting.', self.command) + exit_code = -1 + + return exit_code + + def maybe_translate_repo_uid(self, repo_name): + _org_name = repo_name + if _org_name.startswith('_'): + _org_name = _org_name.split('/', 1)[0] + + if repo_name.startswith('_'): + org_repo_name = repo_name + log.debug('translating UID repo %s', org_repo_name) + by_id_match = call_service_api(self.ini_path, { + 'method': 'service_get_repo_name_by_id', + "args": {"repo_id": repo_name} + }) + if by_id_match: + repo_name = by_id_match['repo_name'] + log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name) + + return repo_name, _org_name + + def serve(self, vcs, repo, mode, user, permissions, branch_permissions): + store = self.repos_path + + check_branch_perms = False + detect_force_push = False + + if branch_permissions: + check_branch_perms = True + detect_force_push = True + + log.debug( + 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s', + vcs, mode, repo, check_branch_perms) + + # detect if we have to check branch permissions + extras = { + 'detect_force_push': detect_force_push, + 'check_branch_perms': check_branch_perms, + 'config': self.ini_path + } + + match vcs: + case 'hg': + server = MercurialServer( + store=store, ini_path=self.ini_path, + repo_name=repo, user=user, + user_permissions=permissions, config=self.config, env=self.env) + case 'git': + server = GitServer( + store=store, ini_path=self.ini_path, + repo_name=repo, repo_mode=mode, user=user, + user_permissions=permissions, config=self.config, env=self.env) + case 'svn': + server = SubversionServer( + store=store, ini_path=self.ini_path, + repo_name=None, user=user, + user_permissions=permissions, config=self.config, env=self.env) + case _: + raise Exception(f'Unrecognised VCS: {vcs}') + self.server_impl = server + return server.run(tunnel_extras=extras) diff --git a/rhodecode/apps/ssh_support/lib/backends/base.py b/rhodecode/apps/ssh_support/lib/backends/base.py index c320d736..620232d7 100644 --- a/rhodecode/apps/ssh_support/lib/backends/base.py +++ b/rhodecode/apps/ssh_support/lib/backends/base.py @@ -23,6 +23,7 @@ import logging from rhodecode.lib.hooks_daemon import prepare_callback_daemon from rhodecode.lib.ext_json import sjson as json from rhodecode.lib.vcs.conf import settings as vcs_settings +from rhodecode.lib.utils import call_service_api from rhodecode.model.scm import ScmModel log = logging.getLogger(__name__) @@ -47,6 +48,7 @@ class VcsServer(object): self.repo_mode = None self.store = '' self.ini_path = '' + self.hooks_protocol = None def _invalidate_cache(self, repo_name): """ @@ -54,7 +56,15 @@ class VcsServer(object): :param repo_name: full repo name, also a cache key """ - ScmModel().mark_for_invalidation(repo_name) + # Todo: Leave only "celery" case after transition. + match self.hooks_protocol: + case 'http': + ScmModel().mark_for_invalidation(repo_name) + case 'celery': + call_service_api(self.ini_path, { + "method": "service_mark_for_invalidation", + "args": {"repo_name": repo_name} + }) def has_write_perm(self): permission = self.user_permissions.get(self.repo_name) @@ -65,30 +75,31 @@ class VcsServer(object): def _check_permissions(self, action): permission = self.user_permissions.get(self.repo_name) + user_info = f'{self.user["user_id"]}:{self.user["username"]}' log.debug('permission for %s on %s are: %s', - self.user, self.repo_name, permission) + user_info, self.repo_name, permission) if not permission: log.error('user `%s` permissions to repo:%s are empty. Forbidding access.', - self.user, self.repo_name) + user_info, self.repo_name) return -2 if action == 'pull': if permission in self.read_perms: log.info( 'READ Permissions for User "%s" detected to repo "%s"!', - self.user, self.repo_name) + user_info, self.repo_name) return 0 else: if permission in self.write_perms: log.info( 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!', - self.user, self.repo_name) + user_info, self.repo_name) return 0 log.error('Cannot properly fetch or verify user `%s` permissions. ' 'Permissions: %s, vcs action: %s', - self.user, permission, action) + user_info, permission, action) return -2 def update_environment(self, action, extras=None): @@ -134,9 +145,10 @@ class VcsServer(object): if exit_code: return exit_code, False - req = self.env['request'] - server_url = req.host_url + req.script_name - extras['server_url'] = server_url + req = self.env.get('request') + if req: + server_url = req.host_url + req.script_name + extras['server_url'] = server_url log.debug('Using %s binaries from path %s', self.backend, self._path) exit_code = self.tunnel.run(extras) @@ -144,12 +156,13 @@ class VcsServer(object): return exit_code, action == "push" def run(self, tunnel_extras=None): + self.hooks_protocol = self.config.get('app:main', 'vcs.hooks.protocol') tunnel_extras = tunnel_extras or {} extras = {} extras.update(tunnel_extras) callback_daemon, extras = prepare_callback_daemon( - extras, protocol=vcs_settings.HOOKS_PROTOCOL, + extras, protocol=self.hooks_protocol, host=vcs_settings.HOOKS_HOST) with callback_daemon: diff --git a/rhodecode/apps/ssh_support/lib/backends/hg.py b/rhodecode/apps/ssh_support/lib/backends/hg.py index 4378a60a..aff94301 100644 --- a/rhodecode/apps/ssh_support/lib/backends/hg.py +++ b/rhodecode/apps/ssh_support/lib/backends/hg.py @@ -23,6 +23,7 @@ import tempfile import textwrap import collections from .base import VcsServer +from rhodecode.lib.utils import call_service_api from rhodecode.model.db import RhodeCodeUi from rhodecode.model.settings import VcsSettingsModel @@ -108,6 +109,14 @@ class MercurialServer(VcsServer): self.tunnel = MercurialTunnelWrapper(server=self) def config_to_hgrc(self, repo_name): + # Todo: once transition is done only call to service api should exist + if self.hooks_protocol == 'celery': + data = call_service_api(self.ini_path, { + "method": "service_config_to_hgrc", + "args": {"cli_flags": self.cli_flags, "repo_name": repo_name} + }) + return data['flags'] + ui_sections = collections.defaultdict(list) ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None) diff --git a/rhodecode/apps/ssh_support/lib/ssh_wrapper.py b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py similarity index 100% rename from rhodecode/apps/ssh_support/lib/ssh_wrapper.py rename to rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py diff --git a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py new file mode 100644 index 00000000..f9383f86 --- /dev/null +++ b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py @@ -0,0 +1,72 @@ +# Copyright (C) 2016-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import os +import sys +import time +import logging + +import click + +from pyramid.paster import setup_logging + +from rhodecode.lib.statsd_client import StatsdClient +from .backends import SshWrapperStandalone +from .ssh_wrapper_v1 import setup_custom_logging + +log = logging.getLogger(__name__) + + +@click.command() +@click.argument('ini_path', type=click.Path(exists=True)) +@click.option( + '--mode', '-m', required=False, default='auto', + type=click.Choice(['auto', 'vcs', 'git', 'hg', 'svn', 'test']), + help='mode of operation') +@click.option('--user', help='Username for which the command will be executed') +@click.option('--user-id', help='User ID for which the command will be executed') +@click.option('--key-id', help='ID of the key from the database') +@click.option('--shell', '-s', is_flag=True, help='Allow Shell') +@click.option('--debug', is_flag=True, help='Enabled detailed output logging') +def main(ini_path, mode, user, user_id, key_id, shell, debug): + setup_custom_logging(ini_path, debug) + + command = os.environ.get('SSH_ORIGINAL_COMMAND', '') + if not command and mode not in ['test']: + raise ValueError( + 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.' + 'Please make sure this is set and available during execution ' + 'of this script.') + connection_info = os.environ.get('SSH_CONNECTION', '') + time_start = time.time() + env = {'RC_CMD_SSH_WRAPPER': '1'} + statsd = StatsdClient.statsd + try: + ssh_wrapper = SshWrapperStandalone( + command, connection_info, mode, + user, user_id, key_id, shell, ini_path, env) + except Exception: + log.exception('Failed to execute SshWrapper') + sys.exit(-5) + return_code = ssh_wrapper.wrap() + operation_took = time.time() - time_start + if statsd: + operation_took_ms = round(1000.0 * operation_took) + statsd.timing("rhodecode_ssh_wrapper_timing.histogram", operation_took_ms, + use_decimals=False) + sys.exit(return_code) diff --git a/rhodecode/apps/ssh_support/tests/conftest.py b/rhodecode/apps/ssh_support/tests/conftest.py index 88c0ca0f..3b581b47 100644 --- a/rhodecode/apps/ssh_support/tests/conftest.py +++ b/rhodecode/apps/ssh_support/tests/conftest.py @@ -20,7 +20,7 @@ import os import pytest import configparser -from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper +from rhodecode.apps.ssh_support.lib.ssh_wrapper_v1 import SshWrapper from rhodecode.lib.utils2 import AttributeDict diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py index 6e15197c..a5f8b11a 100644 --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -34,6 +34,7 @@ import tarfile import warnings from functools import wraps from os.path import join as jn +from configparser import NoOptionError import paste import pkg_resources @@ -52,6 +53,9 @@ from rhodecode.model import meta from rhodecode.model.db import ( Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup) from rhodecode.model.meta import Session +from rhodecode.lib.pyramid_utils import get_config +from rhodecode.lib.vcs import CurlSession +from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError log = logging.getLogger(__name__) @@ -821,3 +825,27 @@ def send_test_email(recipients, email_body='TEST EMAIL'): email_body = email_body_plaintext = email_body subject = f'SUBJECT FROM: {socket.gethostname()}' tasks.send_email(recipients, subject, email_body_plaintext, email_body) + + +def call_service_api(ini_path, payload): + config = get_config(ini_path) + try: + host = config.get('app:main', 'app.service_api.host') + except NoOptionError: + raise ImproperlyConfiguredError( + "app.service_api.host is missing. " + "Please ensure that app.service_api.host and app.service_api.token are " + "defined inside of .ini configuration file." + ) + api_url = config.get('app:main', 'rhodecode.api.url') + payload.update({ + 'id': 'service', + 'auth_token': config.get('app:main', 'app.service_api.token') + }) + + response = CurlSession().post(f'{host}{api_url}', json.dumps(payload)) + + if response.status_code != 200: + raise Exception("Service API responded with error") + + return json.loads(response.content)['result'] diff --git a/rhodecode/lib/vcs/exceptions.py b/rhodecode/lib/vcs/exceptions.py index b95ea5cf..55a1f838 100644 --- a/rhodecode/lib/vcs/exceptions.py +++ b/rhodecode/lib/vcs/exceptions.py @@ -146,6 +146,10 @@ class CommandError(VCSError): pass +class ImproperlyConfiguredError(Exception): + pass + + class UnhandledException(VCSError): """ Signals that something unexpected went wrong. diff --git a/rhodecode/tests/fixture_mods/fixture_pyramid.py b/rhodecode/tests/fixture_mods/fixture_pyramid.py index 3c5999b8..4c3450cd 100644 --- a/rhodecode/tests/fixture_mods/fixture_pyramid.py +++ b/rhodecode/tests/fixture_mods/fixture_pyramid.py @@ -110,6 +110,7 @@ def ini_config(request, tmpdir_factory, rcserver_port, vcsserver_port): 'vcs.scm_app_implementation': 'http', 'vcs.hooks.protocol': 'http', 'vcs.hooks.host': '*', + 'app.service_api.token': 'service_secret_token', }}, {'handler_console': { diff --git a/setup.py b/setup.py index 373cac0f..a5acd918 100644 --- a/setup.py +++ b/setup.py @@ -196,7 +196,8 @@ setup( 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main', 'rc-ishell=rhodecode.lib.rc_commands.ishell:main', 'rc-add-artifact=rhodecode.lib.rc_commands.add_artifact:main', - 'rc-ssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper:main', + 'rc-ssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper_v1:main', + 'rc-ssh-wrapper-v2=rhodecode.apps.ssh_support.lib.ssh_wrapper_v2:main', ], 'beaker.backends': [ 'memorylru_base=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase', From 0b735351409b84b92686901a7c22a7aedcad9ec2 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 27 Feb 2024 09:44:28 +0100 Subject: [PATCH 011/150] settings: remove links to rccontrol from settings and default to the docker image paths --- rhodecode/apps/ssh_support/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rhodecode/apps/ssh_support/__init__.py b/rhodecode/apps/ssh_support/__init__.py index ab536301..fe522ec6 100644 --- a/rhodecode/apps/ssh_support/__init__.py +++ b/rhodecode/apps/ssh_support/__init__.py @@ -42,9 +42,9 @@ def _sanitize_settings_and_apply_defaults(settings): settings_maker.make_setting(config_keys.wrapper_cmd, '') settings_maker.make_setting(config_keys.authorized_keys_line_ssh_opts, '') - settings_maker.make_setting(config_keys.ssh_hg_bin, '~/.rccontrol/vcsserver-1/profile/bin/hg') - settings_maker.make_setting(config_keys.ssh_git_bin, '~/.rccontrol/vcsserver-1/profile/bin/git') - settings_maker.make_setting(config_keys.ssh_svn_bin, '~/.rccontrol/vcsserver-1/profile/bin/svnserve') + settings_maker.make_setting(config_keys.ssh_hg_bin, '/usr/local/bin/rhodecode_bin/vcs_bin/hg') + settings_maker.make_setting(config_keys.ssh_git_bin, '/usr/local/bin/rhodecode_bin/vcs_bin/git') + settings_maker.make_setting(config_keys.ssh_svn_bin, '/usr/local/bin/rhodecode_bin/vcs_bin/svnserve') settings_maker.env_expand() From 2f216c3acf082ef660f4038de87021642b08c271 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 27 Feb 2024 09:48:58 +0100 Subject: [PATCH 012/150] feat(ssh-wrapeprs): moved the setup logging into common function to not relly on v1 codebase --- .../apps/ssh_support/lib/ssh_wrapper_v1.py | 16 +-------- .../apps/ssh_support/lib/ssh_wrapper_v2.py | 4 +-- rhodecode/apps/ssh_support/lib/utils.py | 34 +++++++++++++++++++ 3 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 rhodecode/apps/ssh_support/lib/utils.py diff --git a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py index bf05e723..34da4267 100644 --- a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py +++ b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py @@ -23,28 +23,14 @@ import logging import click -from pyramid.paster import setup_logging - from rhodecode.lib.pyramid_utils import bootstrap from rhodecode.lib.statsd_client import StatsdClient from .backends import SshWrapper +from .utils import setup_custom_logging log = logging.getLogger(__name__) -def setup_custom_logging(ini_path, debug): - if debug: - # enabled rhodecode.ini controlled logging setup - setup_logging(ini_path) - else: - # configure logging in a mode that doesn't print anything. - # in case of regularly configured logging it gets printed out back - # to the client doing an SSH command. - logger = logging.getLogger('') - null = logging.NullHandler() - # add the handler to the root logger - logger.handlers = [null] - @click.command() @click.argument('ini_path', type=click.Path(exists=True)) diff --git a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py index f9383f86..e76a88c5 100644 --- a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py +++ b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py @@ -23,11 +23,9 @@ import logging import click -from pyramid.paster import setup_logging - from rhodecode.lib.statsd_client import StatsdClient from .backends import SshWrapperStandalone -from .ssh_wrapper_v1 import setup_custom_logging +from .utils import setup_custom_logging log = logging.getLogger(__name__) diff --git a/rhodecode/apps/ssh_support/lib/utils.py b/rhodecode/apps/ssh_support/lib/utils.py new file mode 100644 index 00000000..977e4eff --- /dev/null +++ b/rhodecode/apps/ssh_support/lib/utils.py @@ -0,0 +1,34 @@ +# Copyright (C) 2016-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging +from pyramid.paster import setup_logging + + +def setup_custom_logging(ini_path, debug): + if debug: + # enabled rhodecode.ini controlled logging setup + setup_logging(ini_path) + else: + # configure logging in a mode that doesn't print anything. + # in case of regularly configured logging it gets printed out back + # to the client doing an SSH command. + logger = logging.getLogger('') + null = logging.NullHandler() + # add the handler to the root logger + logger.handlers = [null] From 8de3597958480d7df61b7651b455cf232f093aa6 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 27 Feb 2024 09:50:56 +0100 Subject: [PATCH 013/150] fix(api.url): set default api.url and re-use defaults in ssh wrappers --- rhodecode/api/__init__.py | 3 +-- rhodecode/config/middleware.py | 4 ++-- rhodecode/lib/utils.py | 8 +++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/rhodecode/api/__init__.py b/rhodecode/api/__init__.py index 9a2f63e5..965583c3 100644 --- a/rhodecode/api/__init__.py +++ b/rhodecode/api/__init__.py @@ -22,7 +22,6 @@ import sys import fnmatch import decorator -import typing import venusian from collections import OrderedDict @@ -45,7 +44,7 @@ from rhodecode.model.db import User, UserApiKeys log = logging.getLogger(__name__) DEFAULT_RENDERER = 'jsonrpc_renderer' -DEFAULT_URL = '/_admin/apiv2' +DEFAULT_URL = '/_admin/api' SERVICE_API_IDENTIFIER = 'service_' diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py index e21b0b01..aa78fcf7 100644 --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -26,13 +26,13 @@ import logging.config from paste.gzipper import make_gzip_middleware import pyramid.events from pyramid.wsgi import wsgiapp -from pyramid.authorization import ACLAuthorizationPolicy from pyramid.config import Configurator from pyramid.settings import asbool, aslist from pyramid.httpexceptions import ( HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound) from pyramid.renderers import render_to_response +from rhodecode import api from rhodecode.model import meta from rhodecode.config import patches from rhodecode.config import utils as config_utils @@ -527,7 +527,7 @@ def sanitize_settings_and_apply_defaults(global_config, settings): # TODO: johbo: Re-think this, usually the call to config.include # should allow to pass in a prefix. - settings_maker.make_setting('rhodecode.api.url', '/_admin/api') + settings_maker.make_setting('rhodecode.api.url', api.DEFAULT_URL) # Sanitize generic settings. settings_maker.make_setting('default_encoding', 'UTF-8', parser='list') diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py index a5f8b11a..1f8c383f 100644 --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -837,7 +837,13 @@ def call_service_api(ini_path, payload): "Please ensure that app.service_api.host and app.service_api.token are " "defined inside of .ini configuration file." ) - api_url = config.get('app:main', 'rhodecode.api.url') + try: + api_url = config.get('app:main', 'rhodecode.api.url') + except NoOptionError: + from rhodecode import api + log.debug('Cannot find rhodecode.api.url, setting API URL TO Default value') + api_url = api.DEFAULT_URL + payload.update({ 'id': 'service', 'auth_token': config.get('app:main', 'app.service_api.token') From fb6b7a3692890a42c740217baf83f71c8f6154cd Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 27 Feb 2024 09:51:41 +0100 Subject: [PATCH 014/150] chore(imports): optimize imports for ssh events --- rhodecode/apps/admin/views/permissions.py | 2 +- rhodecode/apps/admin/views/users.py | 2 +- rhodecode/apps/my_account/views/my_account_ssh_keys.py | 2 +- rhodecode/apps/ssh_support/__init__.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rhodecode/apps/admin/views/permissions.py b/rhodecode/apps/admin/views/permissions.py index ee13c1cc..14631216 100644 --- a/rhodecode/apps/admin/views/permissions.py +++ b/rhodecode/apps/admin/views/permissions.py @@ -28,7 +28,7 @@ from pyramid.renderers import render from pyramid.response import Response from rhodecode.apps._base import BaseAppView, DataGridAppView -from rhodecode.apps.ssh_support import SshKeyFileChangeEvent +from rhodecode.apps.ssh_support.events import SshKeyFileChangeEvent from rhodecode import events from rhodecode.lib import helpers as h diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py index f58c94ed..f116c77b 100644 --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -27,7 +27,7 @@ from pyramid.response import Response from rhodecode import events from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView -from rhodecode.apps.ssh_support import SshKeyFileChangeEvent +from rhodecode.apps.ssh_support.events import SshKeyFileChangeEvent from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin from rhodecode.authentication.plugins import auth_rhodecode from rhodecode.events import trigger diff --git a/rhodecode/apps/my_account/views/my_account_ssh_keys.py b/rhodecode/apps/my_account/views/my_account_ssh_keys.py index 22788aec..8887f280 100644 --- a/rhodecode/apps/my_account/views/my_account_ssh_keys.py +++ b/rhodecode/apps/my_account/views/my_account_ssh_keys.py @@ -21,7 +21,7 @@ import logging from pyramid.httpexceptions import HTTPFound from rhodecode.apps._base import BaseAppView, DataGridAppView -from rhodecode.apps.ssh_support import SshKeyFileChangeEvent +from rhodecode.apps.ssh_support.events import SshKeyFileChangeEvent from rhodecode.events import trigger from rhodecode.lib import helpers as h from rhodecode.lib import audit_logger diff --git a/rhodecode/apps/ssh_support/__init__.py b/rhodecode/apps/ssh_support/__init__.py index fe522ec6..4adcadbb 100644 --- a/rhodecode/apps/ssh_support/__init__.py +++ b/rhodecode/apps/ssh_support/__init__.py @@ -19,8 +19,6 @@ import logging from . import config_keys -from .events import SshKeyFileChangeEvent -from .subscribers import generate_ssh_authorized_keys_file_subscriber from rhodecode.config.settings_maker import SettingsMaker @@ -55,5 +53,7 @@ def includeme(config): # if we have enable generation of file, subscribe to event if settings[config_keys.generate_authorized_keyfile]: + # lazy import here for faster code reading... via sshwrapper-v2 mode + from .subscribers import generate_ssh_authorized_keys_file_subscriber config.add_subscriber( generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent) From 5d6400eec0692ba798ec42c0a6fec06253fb2a3d Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 27 Feb 2024 09:52:29 +0100 Subject: [PATCH 015/150] fix(service-api): rely on urljoin for constructing the call url --- rhodecode/lib/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py index 1f8c383f..01902210 100644 --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -21,6 +21,7 @@ Utilities library for RhodeCode """ import datetime + import decorator import logging import os @@ -31,6 +32,7 @@ import socket import tempfile import traceback import tarfile +import urllib.parse import warnings from functools import wraps from os.path import join as jn @@ -849,7 +851,7 @@ def call_service_api(ini_path, payload): 'auth_token': config.get('app:main', 'app.service_api.token') }) - response = CurlSession().post(f'{host}{api_url}', json.dumps(payload)) + response = CurlSession().post(urllib.parse.urljoin(host, api_url), json.dumps(payload)) if response.status_code != 200: raise Exception("Service API responded with error") From 93aa6295389bea790f5732f2e992397487138c5f Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 27 Feb 2024 10:32:01 +0100 Subject: [PATCH 016/150] feat(statsd): added extra logging into statsd initalization --- rhodecode/lib/_vendor/statsd/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rhodecode/lib/_vendor/statsd/__init__.py b/rhodecode/lib/_vendor/statsd/__init__.py index 84a54797..2ef9a87c 100644 --- a/rhodecode/lib/_vendor/statsd/__init__.py +++ b/rhodecode/lib/_vendor/statsd/__init__.py @@ -26,9 +26,10 @@ def client_from_config(configuration, prefix='statsd.', **kwargs): from pyramid.settings import asbool _config = statsd_config(configuration, prefix) + statsd_flag = _config.get('enabled') statsd_enabled = asbool(_config.pop('enabled', False)) if not statsd_enabled: - log.debug('statsd client not enabled by statsd.enabled = flag, skipping...') + log.debug('statsd client not enabled by statsd.enabled = %s flag, skipping...', statsd_flag) return host = _config.pop('statsd_host', HOST) From aadbbc076c39500c4bb53a7844dfc0452b527467 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 27 Feb 2024 10:41:51 +0100 Subject: [PATCH 017/150] fix(ssh): added missing import --- rhodecode/apps/ssh_support/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rhodecode/apps/ssh_support/__init__.py b/rhodecode/apps/ssh_support/__init__.py index 4adcadbb..65e330f5 100644 --- a/rhodecode/apps/ssh_support/__init__.py +++ b/rhodecode/apps/ssh_support/__init__.py @@ -55,5 +55,6 @@ def includeme(config): if settings[config_keys.generate_authorized_keyfile]: # lazy import here for faster code reading... via sshwrapper-v2 mode from .subscribers import generate_ssh_authorized_keys_file_subscriber + from .events import SshKeyFileChangeEvent config.add_subscriber( generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent) From 3982ae9102cec1d23fa8abbb707d2355bcffcb76 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 27 Feb 2024 15:37:33 +0100 Subject: [PATCH 018/150] fix(pycurl): added missing __all__ imports --- rhodecode/lib/vcs/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rhodecode/lib/vcs/__init__.py b/rhodecode/lib/vcs/__init__.py index 6b2b8e65..1202d514 100644 --- a/rhodecode/lib/vcs/__init__.py +++ b/rhodecode/lib/vcs/__init__.py @@ -33,7 +33,8 @@ from rhodecode.lib.vcs.exceptions import ( __all__ = [ 'get_vcs_instance', 'get_backend', - 'VCSError', 'RepositoryError', 'CommitError', 'VCSCommunicationError' + 'VCSError', 'RepositoryError', 'CommitError', 'VCSCommunicationError', + 'CurlSession', 'CurlResponse' ] log = logging.getLogger(__name__) From 25f36e96543bc8cb1d7b289efeca756af72b1ad7 Mon Sep 17 00:00:00 2001 From: Serhii Ilin Date: Wed, 28 Feb 2024 09:34:14 +0200 Subject: [PATCH 019/150] feat(svn-config): moved svn related config keys to *.ini file. Fixes: RCCE-60 --- configs/development.ini | 6 +++ configs/production.ini | 6 +++ .../repository/tests/test_repo_summary.py | 37 +++++++++++-------- .../apps/ssh_support/lib/backends/svn.py | 7 ++-- rhodecode/config/middleware.py | 2 + rhodecode/lib/helpers.py | 5 +-- rhodecode/lib/middleware/simplesvn.py | 7 ++-- rhodecode/model/forms.py | 4 -- rhodecode/model/settings.py | 16 -------- rhodecode/templates/base/vcs_settings.mako | 36 ------------------ .../tests/fixture_mods/fixture_pyramid.py | 1 + 11 files changed, 45 insertions(+), 82 deletions(-) diff --git a/configs/development.ini b/configs/development.ini index d7a492a2..7b63786e 100644 --- a/configs/development.ini +++ b/configs/development.ini @@ -615,6 +615,12 @@ vcs.connection_timeout = 3600 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible #vcs.svn.compatible_version = 1.8 +; Enable SVN proxy of requests over HTTP +vcs.svn.proxy.enabled = true + +; host to connect to running SVN subsystem +vcs.svn.proxy.host = http://svn:8090 + ; Cache flag to cache vcsserver remote calls locally ; It uses cache_region `cache_repo` vcs.methods.cache = true diff --git a/configs/production.ini b/configs/production.ini index 6ffeeb7d..75a8de1f 100644 --- a/configs/production.ini +++ b/configs/production.ini @@ -566,6 +566,12 @@ vcs.connection_timeout = 3600 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible #vcs.svn.compatible_version = 1.8 +; Enable SVN proxy of requests over HTTP +vcs.svn.proxy.enabled = true + +; host to connect to running SVN subsystem +vcs.svn.proxy.host = http://svn:8090 + ; Cache flag to cache vcsserver remote calls locally ; It uses cache_region `cache_repo` vcs.methods.cache = true diff --git a/rhodecode/apps/repository/tests/test_repo_summary.py b/rhodecode/apps/repository/tests/test_repo_summary.py index deb90971..8fb75b34 100644 --- a/rhodecode/apps/repository/tests/test_repo_summary.py +++ b/rhodecode/apps/repository/tests/test_repo_summary.py @@ -1,4 +1,3 @@ - # Copyright (C) 2010-2023 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify @@ -52,13 +51,13 @@ def assert_clone_url(response, server, repo, disabled=False): @pytest.mark.usefixtures('app') class TestSummaryView(object): + def test_index(self, autologin_user, backend, http_host_only_stub): repo_id = backend.repo.repo_id repo_name = backend.repo_name - with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', - return_value=False): - response = self.app.get( - route_path('repo_summary', repo_name=repo_name)) + + response = self.app.get( + route_path('repo_summary', repo_name=repo_name)) # repo type response.mustcontain( @@ -71,37 +70,43 @@ class TestSummaryView(object): # clone url... assert_clone_url(response, http_host_only_stub, repo_name) - assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id)) + assert_clone_url(response, http_host_only_stub, f'_{repo_id}') def test_index_svn_without_proxy( self, autologin_user, backend_svn, http_host_only_stub): + repo_id = backend_svn.repo.repo_id repo_name = backend_svn.repo_name - response = self.app.get(route_path('repo_summary', repo_name=repo_name)) - # clone url... + # by default the SVN is enabled now, this is how inputs look when it's disabled + with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', return_value=True): + + response = self.app.get( + route_path('repo_summary', repo_name=repo_name), + status=200) + + # clone url test... assert_clone_url(response, http_host_only_stub, repo_name, disabled=True) - assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id), disabled=True) + assert_clone_url(response, http_host_only_stub, f'_{repo_id}', disabled=True) def test_index_with_trailing_slash( self, autologin_user, backend, http_host_only_stub): repo_id = backend.repo.repo_id repo_name = backend.repo_name - with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', - return_value=False): - response = self.app.get( - route_path('repo_summary', repo_name=repo_name) + '/', - status=200) + trailing_slash = '/' + response = self.app.get( + route_path('repo_summary', repo_name=repo_name) + trailing_slash, + status=200) # clone url... assert_clone_url(response, http_host_only_stub, repo_name) - assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id)) + assert_clone_url(response, http_host_only_stub, f'_{repo_id}') def test_index_by_id(self, autologin_user, backend): repo_id = backend.repo.repo_id response = self.app.get( - route_path('repo_summary', repo_name='_%s' % (repo_id,))) + route_path('repo_summary', repo_name=f'_{repo_id}')) # repo type response.mustcontain( diff --git a/rhodecode/apps/ssh_support/lib/backends/svn.py b/rhodecode/apps/ssh_support/lib/backends/svn.py index 728ec8ec..6d4ea5de 100644 --- a/rhodecode/apps/ssh_support/lib/backends/svn.py +++ b/rhodecode/apps/ssh_support/lib/backends/svn.py @@ -244,9 +244,10 @@ class SubversionServer(VcsServer): # if exit_code: # return exit_code, False - req = self.env['request'] - server_url = req.host_url + req.script_name - extras['server_url'] = server_url + req = self.env.get('request') + if req: + server_url = req.host_url + req.script_name + extras['server_url'] = server_url log.debug('Using %s binaries from path %s', self.backend, self._path) exit_code = self.tunnel.run(extras) diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py index e21b0b01..2962adee 100644 --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -542,6 +542,8 @@ def sanitize_settings_and_apply_defaults(global_config, settings): settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool') settings_maker.make_setting('vcs.svn.compatible_version', '') + settings_maker.make_setting('vcs.svn.proxy.enabled', 'true', parser='bool') + settings_maker.make_setting('vcs.svn.proxy.host', 'http://svn:8090') settings_maker.make_setting('vcs.hooks.protocol', 'http') settings_maker.make_setting('vcs.hooks.host', '*') settings_maker.make_setting('vcs.scm_app_implementation', 'http') diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py index d8fb96f8..62e05336 100644 --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -74,6 +74,7 @@ from webhelpers2.html.tags import ( from webhelpers2.number import format_byte_size # python3.11 backport fixes for webhelpers2 +from rhodecode import ConfigGet from rhodecode.lib._vendor.webhelpers_backports import raw_select from rhodecode.lib.action_parser import action_parser @@ -916,9 +917,7 @@ def get_repo_type_by_name(repo_name): def is_svn_without_proxy(repository): if is_svn(repository): - from rhodecode.model.settings import VcsSettingsModel - conf = VcsSettingsModel().get_ui_settings_as_config_obj() - return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled')) + return not ConfigGet().get_bool('vcs.svn.proxy.enabled') return False diff --git a/rhodecode/lib/middleware/simplesvn.py b/rhodecode/lib/middleware/simplesvn.py index 451e1708..0d42e179 100644 --- a/rhodecode/lib/middleware/simplesvn.py +++ b/rhodecode/lib/middleware/simplesvn.py @@ -27,6 +27,7 @@ import urllib.parse import requests from pyramid.httpexceptions import HTTPNotAcceptable +from rhodecode import ConfigGet from rhodecode.lib import rc_cache from rhodecode.lib.middleware import simplevcs from rhodecode.lib.middleware.utils import get_path_info @@ -232,12 +233,10 @@ class SimpleSvn(simplevcs.SimpleVCS): return DisabledSimpleSvnApp(config) def _is_svn_enabled(self): - conf = self.repo_vcs_config - return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled')) + return ConfigGet().get_bool('vcs.svn.proxy.enabled') def _create_config(self, extras, repo_name, scheme='http'): - conf = self.repo_vcs_config - server_url = conf.get('vcs_svn_proxy', 'http_server_url') + server_url = ConfigGet().get_str('vcs.svn.proxy.host') server_url = server_url or self.DEFAULT_HTTP_SERVER extras['subversion_http_server_url'] = server_url diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index 39d3b21c..e4b75723 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -421,10 +421,6 @@ class _BaseVcsSettingsForm(formencode.Schema): rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False) rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False) - # svn - vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False) - vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None) - # cache rhodecode_diff_cache = v.StringBoolean(if_missing=False) diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py index 0beff5fe..1879fa2a 100644 --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -499,11 +499,6 @@ class VcsSettingsModel(object): ('vcs_git_lfs', 'store_location') ) - GLOBAL_SVN_SETTINGS = ( - ('vcs_svn_proxy', 'http_requests_enabled'), - ('vcs_svn_proxy', 'http_server_url') - ) - SVN_BRANCH_SECTION = 'vcs_svn_branch' SVN_TAG_SECTION = 'vcs_svn_tag' SSL_SETTING = ('web', 'push_ssl') @@ -718,17 +713,6 @@ class VcsSettingsModel(object): # branch/tags patterns self._create_svn_settings(self.global_settings, data) - http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS - http_requests_enabled_key, http_server_url_key = self._get_settings_keys( - self.GLOBAL_SVN_SETTINGS, data) - - self._create_or_update_ui( - self.global_settings, *http_requests_enabled, - value=safe_str(data[http_requests_enabled_key])) - self._create_or_update_ui( - self.global_settings, *http_server_url, - value=data[http_server_url_key]) - def update_global_ssl_setting(self, value): self._create_or_update_ui( self.global_settings, *self.SSL_SETTING, value=value) diff --git a/rhodecode/templates/base/vcs_settings.mako b/rhodecode/templates/base/vcs_settings.mako index d9b80493..3cfec674 100644 --- a/rhodecode/templates/base/vcs_settings.mako +++ b/rhodecode/templates/base/vcs_settings.mako @@ -170,42 +170,6 @@ % endif - - % if display_globals: -
-
-

${_('Global Subversion Settings')}

-
-
-
-
- ${h.checkbox('vcs_svn_proxy_http_requests_enabled' + suffix, 'True', **kwargs)} - -
-
- - ${_('Subversion HTTP Support. Enables communication with SVN over HTTP protocol.')} - ${_('SVN Protocol setup Documentation')}. - -
-
-
-
-
-
-
- ${h.text('vcs_svn_proxy_http_server_url',size=59)} - % if c.svn_proxy_generate_config: - - - - % endif -
-
-
-
- % endif - % if display_globals or repo_type in ['svn']:
diff --git a/rhodecode/tests/fixture_mods/fixture_pyramid.py b/rhodecode/tests/fixture_mods/fixture_pyramid.py index 4c3450cd..b896f4f0 100644 --- a/rhodecode/tests/fixture_mods/fixture_pyramid.py +++ b/rhodecode/tests/fixture_mods/fixture_pyramid.py @@ -108,6 +108,7 @@ def ini_config(request, tmpdir_factory, rcserver_port, vcsserver_port): 'vcs.server.protocol': 'http', 'vcs.scm_app_implementation': 'http', + 'vcs.svn.proxy.enabled': 'true', 'vcs.hooks.protocol': 'http', 'vcs.hooks.host': '*', 'app.service_api.token': 'service_secret_token', From cdc40e48c7f75d774fd6a87dc71d3ee9d7858c97 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 28 Feb 2024 09:29:49 +0100 Subject: [PATCH 020/150] statsd: added special repr for statsdclient for nicer debug --- rhodecode/lib/statsd_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rhodecode/lib/statsd_client.py b/rhodecode/lib/statsd_client.py index 7cc0a96d..a5959f83 100644 --- a/rhodecode/lib/statsd_client.py +++ b/rhodecode/lib/statsd_client.py @@ -25,6 +25,9 @@ class StatsdClientClass(Singleton): statsd_client = None statsd = None + def __repr__(self): + return f"{self.__class__}(statsd={self.statsd})" + def __getattribute__(self, name): if name.startswith("statsd"): From d57482be6221b6c89b3aa0462457a3c802cf6c31 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 28 Feb 2024 09:30:11 +0100 Subject: [PATCH 021/150] fix(pycurl): report nicer error log on failed calls in pycurl --- rhodecode/lib/vcs/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rhodecode/lib/vcs/__init__.py b/rhodecode/lib/vcs/__init__.py index 1202d514..da7de79b 100644 --- a/rhodecode/lib/vcs/__init__.py +++ b/rhodecode/lib/vcs/__init__.py @@ -136,7 +136,12 @@ class CurlSession(object): curl.setopt(curl.FOLLOWLOCATION, allow_redirects) curl.setopt(curl.WRITEDATA, response_buffer) curl.setopt(curl.HTTPHEADER, headers_list) - curl.perform() + + try: + curl.perform() + except pycurl.error as exc: + log.error('Failed to call endpoint url: {} using pycurl'.format(url)) + raise status_code = curl.getinfo(pycurl.HTTP_CODE) content_type = curl.getinfo(pycurl.CONTENT_TYPE) From fc0ee0e99bd416bba464f6e5809e605cb1341310 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 28 Feb 2024 09:30:58 +0100 Subject: [PATCH 022/150] feat(ssh-wrapper-speedup): major rewrite of code to address imports problem with ssh-wrapper-v2 - use bootstrapped settings rather than config - use more code split to make sure we don't import heavy code --- .../apps/ssh_support/lib/backends/__init__.py | 63 ++++-- .../apps/ssh_support/lib/backends/base.py | 23 +- .../apps/ssh_support/lib/backends/git.py | 12 +- rhodecode/apps/ssh_support/lib/backends/hg.py | 85 ++++---- .../apps/ssh_support/lib/backends/svn.py | 12 +- .../apps/ssh_support/lib/ssh_wrapper_v1.py | 4 +- .../apps/ssh_support/lib/ssh_wrapper_v2.py | 32 ++- rhodecode/apps/ssh_support/lib/utils.py | 2 +- rhodecode/apps/ssh_support/tests/conftest.py | 7 +- .../apps/ssh_support/tests/test_server_git.py | 10 +- .../apps/ssh_support/tests/test_server_hg.py | 7 +- .../apps/ssh_support/tests/test_server_svn.py | 7 +- .../ssh_support/tests/test_ssh_wrapper.py | 4 - rhodecode/config/config_maker.py | 198 ++++++++++++++++++ rhodecode/config/middleware.py | 177 +--------------- rhodecode/config/utils.py | 3 +- rhodecode/lib/api_utils.py | 38 ++++ rhodecode/lib/base.py | 2 +- rhodecode/lib/config_utils.py | 40 ++++ rhodecode/lib/hook_daemon/__init__.py | 17 ++ rhodecode/lib/hook_daemon/base.py | 115 ++++++++++ .../lib/hook_daemon/celery_hooks_deamon.py | 30 +++ rhodecode/lib/hook_daemon/hook_module.py | 104 +++++++++ .../http_hooks_deamon.py} | 177 +--------------- rhodecode/lib/middleware/simplesvn.py | 3 +- rhodecode/lib/middleware/simplevcs.py | 2 +- rhodecode/lib/pyramid_utils.py | 16 +- rhodecode/lib/rc_commands/setup_rc.py | 3 +- rhodecode/lib/utils.py | 37 +--- rhodecode/model/pull_request.py | 2 +- .../tests/fixture_mods/fixture_pyramid.py | 2 +- rhodecode/tests/fixture_mods/fixture_utils.py | 2 +- rhodecode/tests/lib/test_hooks_daemon.py | 90 ++++---- 33 files changed, 764 insertions(+), 562 deletions(-) create mode 100644 rhodecode/config/config_maker.py create mode 100644 rhodecode/lib/api_utils.py create mode 100644 rhodecode/lib/config_utils.py create mode 100644 rhodecode/lib/hook_daemon/__init__.py create mode 100644 rhodecode/lib/hook_daemon/base.py create mode 100644 rhodecode/lib/hook_daemon/celery_hooks_deamon.py create mode 100644 rhodecode/lib/hook_daemon/hook_module.py rename rhodecode/lib/{hooks_daemon.py => hook_daemon/http_hooks_deamon.py} (61%) diff --git a/rhodecode/apps/ssh_support/lib/backends/__init__.py b/rhodecode/apps/ssh_support/lib/backends/__init__.py index d4479be6..f0a3e80e 100644 --- a/rhodecode/apps/ssh_support/lib/backends/__init__.py +++ b/rhodecode/apps/ssh_support/lib/backends/__init__.py @@ -20,12 +20,11 @@ import os import re import logging import datetime -import configparser from sqlalchemy import Table -from rhodecode.lib.utils import call_service_api +from rhodecode.lib.api_utils import call_service_api from rhodecode.lib.utils2 import AttributeDict -from rhodecode.model.scm import ScmModel +from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError from .hg import MercurialServer from .git import GitServer @@ -39,7 +38,7 @@ class SshWrapper(object): svn_cmd_pat = re.compile(r'^svnserve -t') def __init__(self, command, connection_info, mode, - user, user_id, key_id: int, shell, ini_path: str, env): + user, user_id, key_id: int, shell, ini_path: str, settings, env): self.command = command self.connection_info = connection_info self.mode = mode @@ -49,15 +48,9 @@ class SshWrapper(object): self.shell = shell self.ini_path = ini_path self.env = env - - self.config = self.parse_config(ini_path) + self.settings = settings self.server_impl = None - def parse_config(self, config_path): - parser = configparser.ConfigParser() - parser.read(config_path) - return parser - def update_key_access_time(self, key_id): from rhodecode.model.meta import raw_query_executor, Base @@ -162,6 +155,9 @@ class SshWrapper(object): return vcs_type, repo_name, mode def serve(self, vcs, repo, mode, user, permissions, branch_permissions): + # TODO: remove this once we have .ini defined access path... + from rhodecode.model.scm import ScmModel + store = ScmModel().repos_path check_branch_perms = False @@ -186,7 +182,7 @@ class SshWrapper(object): server = MercurialServer( store=store, ini_path=self.ini_path, repo_name=repo, user=user, - user_permissions=permissions, config=self.config, env=self.env) + user_permissions=permissions, settings=self.settings, env=self.env) self.server_impl = server return server.run(tunnel_extras=extras) @@ -194,7 +190,7 @@ class SshWrapper(object): server = GitServer( store=store, ini_path=self.ini_path, repo_name=repo, repo_mode=mode, user=user, - user_permissions=permissions, config=self.config, env=self.env) + user_permissions=permissions, settings=self.settings, env=self.env) self.server_impl = server return server.run(tunnel_extras=extras) @@ -202,7 +198,7 @@ class SshWrapper(object): server = SubversionServer( store=store, ini_path=self.ini_path, repo_name=None, user=user, - user_permissions=permissions, config=self.config, env=self.env) + user_permissions=permissions, settings=self.settings, env=self.env) self.server_impl = server return server.run(tunnel_extras=extras) @@ -269,6 +265,35 @@ class SshWrapperStandalone(SshWrapper): New version of SshWrapper designed to be depended only on service API """ repos_path = None + service_api_host: str + service_api_token: str + api_url: str + + def __init__(self, command, connection_info, mode, + user, user_id, key_id: int, shell, ini_path: str, settings, env): + + # validate our settings for making a standalone calls + try: + self.service_api_host = settings['app.service_api.host'] + self.service_api_token = settings['app.service_api.token'] + except KeyError: + raise ImproperlyConfiguredError( + "app.service_api.host or app.service_api.token are missing. " + "Please ensure that app.service_api.host and app.service_api.token are " + "defined inside of .ini configuration file." + ) + + try: + self.api_url = settings['rhodecode.api.url'] + except KeyError: + raise ImproperlyConfiguredError( + "rhodecode.api.url is missing. " + "Please ensure that rhodecode.api.url is " + "defined inside of .ini configuration file." + ) + + super(SshWrapperStandalone, self).__init__( + command, connection_info, mode, user, user_id, key_id, shell, ini_path, settings, env) @staticmethod def parse_user_related_data(user_data): @@ -301,7 +326,7 @@ class SshWrapperStandalone(SshWrapper): exit_code = 1 elif scm_detected: - data = call_service_api(self.ini_path, { + data = call_service_api(self.service_api_host, self.service_api_token, self.api_url, { "method": "service_get_data_for_ssh_wrapper", "args": {"user_id": user_id, "repo_name": scm_repo, "key_id": self.key_id} }) @@ -339,7 +364,7 @@ class SshWrapperStandalone(SshWrapper): if repo_name.startswith('_'): org_repo_name = repo_name log.debug('translating UID repo %s', org_repo_name) - by_id_match = call_service_api(self.ini_path, { + by_id_match = call_service_api(self.service_api_host, self.service_api_token, self.api_url, { 'method': 'service_get_repo_name_by_id', "args": {"repo_id": repo_name} }) @@ -375,17 +400,17 @@ class SshWrapperStandalone(SshWrapper): server = MercurialServer( store=store, ini_path=self.ini_path, repo_name=repo, user=user, - user_permissions=permissions, config=self.config, env=self.env) + user_permissions=permissions, settings=self.settings, env=self.env) case 'git': server = GitServer( store=store, ini_path=self.ini_path, repo_name=repo, repo_mode=mode, user=user, - user_permissions=permissions, config=self.config, env=self.env) + user_permissions=permissions, settings=self.settings, env=self.env) case 'svn': server = SubversionServer( store=store, ini_path=self.ini_path, repo_name=None, user=user, - user_permissions=permissions, config=self.config, env=self.env) + user_permissions=permissions, settings=self.settings, env=self.env) case _: raise Exception(f'Unrecognised VCS: {vcs}') self.server_impl = server diff --git a/rhodecode/apps/ssh_support/lib/backends/base.py b/rhodecode/apps/ssh_support/lib/backends/base.py index 620232d7..46ffe33d 100644 --- a/rhodecode/apps/ssh_support/lib/backends/base.py +++ b/rhodecode/apps/ssh_support/lib/backends/base.py @@ -20,27 +20,27 @@ import os import sys import logging -from rhodecode.lib.hooks_daemon import prepare_callback_daemon +from rhodecode.lib.hook_daemon.base import prepare_callback_daemon from rhodecode.lib.ext_json import sjson as json from rhodecode.lib.vcs.conf import settings as vcs_settings -from rhodecode.lib.utils import call_service_api -from rhodecode.model.scm import ScmModel +from rhodecode.lib.api_utils import call_service_api log = logging.getLogger(__name__) -class VcsServer(object): +class SSHVcsServer(object): repo_user_agent = None # set in child classes _path = None # set executable path for hg/git/svn binary backend = None # set in child classes tunnel = None # subprocess handling tunnel + settings = None # parsed settings module write_perms = ['repository.admin', 'repository.write'] read_perms = ['repository.read', 'repository.admin', 'repository.write'] - def __init__(self, user, user_permissions, config, env): + def __init__(self, user, user_permissions, settings, env): self.user = user self.user_permissions = user_permissions - self.config = config + self.settings = settings self.env = env self.stdin = sys.stdin @@ -59,9 +59,14 @@ class VcsServer(object): # Todo: Leave only "celery" case after transition. match self.hooks_protocol: case 'http': + from rhodecode.model.scm import ScmModel ScmModel().mark_for_invalidation(repo_name) case 'celery': - call_service_api(self.ini_path, { + service_api_host = self.settings['app.service_api.host'] + service_api_token = self.settings['app.service_api.token'] + api_url = self.settings['rhodecode.api.url'] + + call_service_api(service_api_host, service_api_token, api_url, { "method": "service_mark_for_invalidation", "args": {"repo_name": repo_name} }) @@ -118,7 +123,7 @@ class VcsServer(object): 'server_url': None, 'user_agent': f'{self.repo_user_agent}/ssh-user-agent', 'hooks': ['push', 'pull'], - 'hooks_module': 'rhodecode.lib.hooks_daemon', + 'hooks_module': 'rhodecode.lib.hook_daemon.hook_module', 'is_shadow_repo': False, 'detect_force_push': False, 'check_branch_perms': False, @@ -156,7 +161,7 @@ class VcsServer(object): return exit_code, action == "push" def run(self, tunnel_extras=None): - self.hooks_protocol = self.config.get('app:main', 'vcs.hooks.protocol') + self.hooks_protocol = self.settings['vcs.hooks.protocol'] tunnel_extras = tunnel_extras or {} extras = {} extras.update(tunnel_extras) diff --git a/rhodecode/apps/ssh_support/lib/backends/git.py b/rhodecode/apps/ssh_support/lib/backends/git.py index 5ee88bab..ffedfb85 100644 --- a/rhodecode/apps/ssh_support/lib/backends/git.py +++ b/rhodecode/apps/ssh_support/lib/backends/git.py @@ -21,7 +21,7 @@ import logging import subprocess from vcsserver import hooks -from .base import VcsServer +from .base import SSHVcsServer log = logging.getLogger(__name__) @@ -70,19 +70,17 @@ class GitTunnelWrapper(object): return result -class GitServer(VcsServer): +class GitServer(SSHVcsServer): backend = 'git' repo_user_agent = 'git' - def __init__(self, store, ini_path, repo_name, repo_mode, - user, user_permissions, config, env): - super().\ - __init__(user, user_permissions, config, env) + def __init__(self, store, ini_path, repo_name, repo_mode, user, user_permissions, settings, env): + super().__init__(user, user_permissions, settings, env) self.store = store self.ini_path = ini_path self.repo_name = repo_name - self._path = self.git_path = config.get('app:main', 'ssh.executable.git') + self._path = self.git_path = settings['ssh.executable.git'] self.repo_mode = repo_mode self.tunnel = GitTunnelWrapper(server=self) diff --git a/rhodecode/apps/ssh_support/lib/backends/hg.py b/rhodecode/apps/ssh_support/lib/backends/hg.py index aff94301..e7f7ed80 100644 --- a/rhodecode/apps/ssh_support/lib/backends/hg.py +++ b/rhodecode/apps/ssh_support/lib/backends/hg.py @@ -22,10 +22,10 @@ import logging import tempfile import textwrap import collections -from .base import VcsServer -from rhodecode.lib.utils import call_service_api -from rhodecode.model.db import RhodeCodeUi -from rhodecode.model.settings import VcsSettingsModel + +from .base import SSHVcsServer + +from rhodecode.lib.api_utils import call_service_api log = logging.getLogger(__name__) @@ -57,7 +57,7 @@ class MercurialTunnelWrapper(object): # cleanup custom hgrc file if os.path.isfile(hgrc_custom): with open(hgrc_custom, 'wb') as f: - f.write('') + f.write(b'') log.debug('Cleanup custom hgrc file under %s', hgrc_custom) # write temp @@ -94,62 +94,67 @@ class MercurialTunnelWrapper(object): self.remove_configs() -class MercurialServer(VcsServer): +class MercurialServer(SSHVcsServer): backend = 'hg' repo_user_agent = 'mercurial' cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks'] - def __init__(self, store, ini_path, repo_name, user, user_permissions, config, env): - super().__init__(user, user_permissions, config, env) + def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env): + super().__init__(user, user_permissions, settings, env) self.store = store self.ini_path = ini_path self.repo_name = repo_name - self._path = self.hg_path = config.get('app:main', 'ssh.executable.hg') + self._path = self.hg_path = settings['ssh.executable.hg'] self.tunnel = MercurialTunnelWrapper(server=self) def config_to_hgrc(self, repo_name): # Todo: once transition is done only call to service api should exist if self.hooks_protocol == 'celery': - data = call_service_api(self.ini_path, { + service_api_host = self.settings['app.service_api.host'] + service_api_token = self.settings['app.service_api.token'] + api_url = self.settings['rhodecode.api.url'] + data = call_service_api(service_api_host, service_api_token, api_url, { "method": "service_config_to_hgrc", "args": {"cli_flags": self.cli_flags, "repo_name": repo_name} }) return data['flags'] + else: + from rhodecode.model.db import RhodeCodeUi + from rhodecode.model.settings import VcsSettingsModel + ui_sections = collections.defaultdict(list) + ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None) - ui_sections = collections.defaultdict(list) - ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None) + # write default hooks + default_hooks = [ + ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'), + ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'), + ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'), - # write default hooks - default_hooks = [ - ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'), - ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'), - ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'), + ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'), + ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'), + ] - ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'), - ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'), - ] + for k, v in default_hooks: + ui_sections['hooks'].append((k, v)) - for k, v in default_hooks: - ui_sections['hooks'].append((k, v)) - - for entry in ui: - if not entry.active: - continue - sec = entry.section - key = entry.key - - if sec in self.cli_flags: - # we want only custom hooks, so we skip builtins - if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN: + for entry in ui: + if not entry.active: continue + sec = entry.section + key = entry.key - ui_sections[sec].append([key, entry.value]) + if sec in self.cli_flags: + # we want only custom hooks, so we skip builtins + if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN: + continue - flags = [] - for _sec, key_val in ui_sections.items(): - flags.append(' ') - flags.append(f'[{_sec}]') - for key, val in key_val: - flags.append(f'{key}= {val}') - return flags + ui_sections[sec].append([key, entry.value]) + + flags = [] + for _sec, key_val in ui_sections.items(): + flags.append(' ') + flags.append(f'[{_sec}]') + for key, val in key_val: + flags.append(f'{key}= {val}') + return flags diff --git a/rhodecode/apps/ssh_support/lib/backends/svn.py b/rhodecode/apps/ssh_support/lib/backends/svn.py index 728ec8ec..e7fefc29 100644 --- a/rhodecode/apps/ssh_support/lib/backends/svn.py +++ b/rhodecode/apps/ssh_support/lib/backends/svn.py @@ -25,7 +25,7 @@ import tempfile from subprocess import Popen, PIPE import urllib.parse -from .base import VcsServer +from .base import SSHVcsServer log = logging.getLogger(__name__) @@ -218,20 +218,18 @@ class SubversionTunnelWrapper(object): return self.return_code -class SubversionServer(VcsServer): +class SubversionServer(SSHVcsServer): backend = 'svn' repo_user_agent = 'svn' - def __init__(self, store, ini_path, repo_name, - user, user_permissions, config, env): - super()\ - .__init__(user, user_permissions, config, env) + def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env): + super().__init__(user, user_permissions, settings, env) self.store = store self.ini_path = ini_path # NOTE(dan): repo_name at this point is empty, # this is set later in .run() based from parsed input stream self.repo_name = repo_name - self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn') + self._path = self.svn_path = settings['ssh.executable.svn'] self.tunnel = SubversionTunnelWrapper(server=self) diff --git a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py index 34da4267..1fd567ed 100644 --- a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py +++ b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py @@ -31,7 +31,6 @@ from .utils import setup_custom_logging log = logging.getLogger(__name__) - @click.command() @click.argument('ini_path', type=click.Path(exists=True)) @click.option( @@ -55,11 +54,12 @@ def main(ini_path, mode, user, user_id, key_id, shell, debug): connection_info = os.environ.get('SSH_CONNECTION', '') time_start = time.time() with bootstrap(ini_path, env={'RC_CMD_SSH_WRAPPER': '1'}) as env: + settings = env['registry'].settings statsd = StatsdClient.statsd try: ssh_wrapper = SshWrapper( command, connection_info, mode, - user, user_id, key_id, shell, ini_path, env) + user, user_id, key_id, shell, ini_path, settings, env) except Exception: log.exception('Failed to execute SshWrapper') sys.exit(-5) diff --git a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py index e76a88c5..fff01ca3 100644 --- a/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py +++ b/rhodecode/apps/ssh_support/lib/ssh_wrapper_v2.py @@ -16,6 +16,14 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +""" +WARNING: be really carefully with changing ANY imports in this file +# This script is to mean as really fast executable, doing some imports here that would yield an import chain change +# can affect execution times... +# This can be easily debugged using such command:: +# time PYTHONPROFILEIMPORTTIME=1 rc-ssh-wrapper-v2 --debug --mode=test .dev/dev.ini +""" + import os import sys import time @@ -23,9 +31,12 @@ import logging import click +from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults from rhodecode.lib.statsd_client import StatsdClient -from .backends import SshWrapperStandalone +from rhodecode.lib.config_utils import get_app_config_lightweight + from .utils import setup_custom_logging +from .backends import SshWrapperStandalone log = logging.getLogger(__name__) @@ -42,6 +53,8 @@ log = logging.getLogger(__name__) @click.option('--shell', '-s', is_flag=True, help='Allow Shell') @click.option('--debug', is_flag=True, help='Enabled detailed output logging') def main(ini_path, mode, user, user_id, key_id, shell, debug): + + time_start = time.time() setup_custom_logging(ini_path, debug) command = os.environ.get('SSH_ORIGINAL_COMMAND', '') @@ -50,21 +63,30 @@ def main(ini_path, mode, user, user_id, key_id, shell, debug): 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.' 'Please make sure this is set and available during execution ' 'of this script.') - connection_info = os.environ.get('SSH_CONNECTION', '') - time_start = time.time() - env = {'RC_CMD_SSH_WRAPPER': '1'} + + # initialize settings and get defaults + settings = get_app_config_lightweight(ini_path) + settings = sanitize_settings_and_apply_defaults({'__file__': ini_path}, settings) + + # init and bootstrap StatsdClient + StatsdClient.setup(settings) statsd = StatsdClient.statsd + try: + connection_info = os.environ.get('SSH_CONNECTION', '') + env = {'RC_CMD_SSH_WRAPPER': '1'} ssh_wrapper = SshWrapperStandalone( command, connection_info, mode, - user, user_id, key_id, shell, ini_path, env) + user, user_id, key_id, shell, ini_path, settings, env) except Exception: log.exception('Failed to execute SshWrapper') sys.exit(-5) + return_code = ssh_wrapper.wrap() operation_took = time.time() - time_start if statsd: operation_took_ms = round(1000.0 * operation_took) statsd.timing("rhodecode_ssh_wrapper_timing.histogram", operation_took_ms, use_decimals=False) + sys.exit(return_code) diff --git a/rhodecode/apps/ssh_support/lib/utils.py b/rhodecode/apps/ssh_support/lib/utils.py index 977e4eff..bfe91082 100644 --- a/rhodecode/apps/ssh_support/lib/utils.py +++ b/rhodecode/apps/ssh_support/lib/utils.py @@ -17,11 +17,11 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import logging -from pyramid.paster import setup_logging def setup_custom_logging(ini_path, debug): if debug: + from pyramid.paster import setup_logging # Lazy import # enabled rhodecode.ini controlled logging setup setup_logging(ini_path) else: diff --git a/rhodecode/apps/ssh_support/tests/conftest.py b/rhodecode/apps/ssh_support/tests/conftest.py index 3b581b47..7c5a90cf 100644 --- a/rhodecode/apps/ssh_support/tests/conftest.py +++ b/rhodecode/apps/ssh_support/tests/conftest.py @@ -52,7 +52,10 @@ def dummy_env(): def plain_dummy_user(): - return AttributeDict(username='test_user') + return AttributeDict( + user_id=1, + username='test_user' + ) @pytest.fixture() @@ -65,4 +68,4 @@ def ssh_wrapper(app, dummy_conf_file, dummy_env): conn_info = '127.0.0.1 22 10.0.0.1 443' return SshWrapper( 'random command', conn_info, 'auto', 'admin', '1', key_id='1', - shell=False, ini_path=dummy_conf_file, env=dummy_env) + shell=False, ini_path=dummy_conf_file, settings={}, env=dummy_env) diff --git a/rhodecode/apps/ssh_support/tests/test_server_git.py b/rhodecode/apps/ssh_support/tests/test_server_git.py index bb8e2088..175163f9 100644 --- a/rhodecode/apps/ssh_support/tests/test_server_git.py +++ b/rhodecode/apps/ssh_support/tests/test_server_git.py @@ -25,6 +25,7 @@ from rhodecode.apps.ssh_support.lib.backends.git import GitServer from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user from rhodecode.lib.ext_json import json + class GitServerCreator(object): root = '/tmp/repo/path/' git_path = '/usr/local/bin/git' @@ -39,10 +40,7 @@ class GitServerCreator(object): user = plain_dummy_user() def __init__(self): - def config_get(part, key): - return self.config_data.get(part, {}).get(key) - self.config_mock = mock.Mock() - self.config_mock.get = mock.Mock(side_effect=config_get) + pass def create(self, **kwargs): parameters = { @@ -54,7 +52,7 @@ class GitServerCreator(object): 'user_permissions': { self.repo_name: 'repository.admin' }, - 'config': self.config_mock, + 'settings': self.config_data['app:main'], 'env': plain_dummy_env() } parameters.update(kwargs) @@ -142,7 +140,7 @@ class TestGitServer(object): 'server_url': None, 'hooks': ['push', 'pull'], 'is_shadow_repo': False, - 'hooks_module': 'rhodecode.lib.hooks_daemon', + 'hooks_module': 'rhodecode.lib.hook_daemon.hook_module', 'check_branch_perms': False, 'detect_force_push': False, 'user_agent': u'git/ssh-user-agent', diff --git a/rhodecode/apps/ssh_support/tests/test_server_hg.py b/rhodecode/apps/ssh_support/tests/test_server_hg.py index ee1b1af6..031f932d 100644 --- a/rhodecode/apps/ssh_support/tests/test_server_hg.py +++ b/rhodecode/apps/ssh_support/tests/test_server_hg.py @@ -38,10 +38,7 @@ class MercurialServerCreator(object): user = plain_dummy_user() def __init__(self): - def config_get(part, key): - return self.config_data.get(part, {}).get(key) - self.config_mock = mock.Mock() - self.config_mock.get = mock.Mock(side_effect=config_get) + pass def create(self, **kwargs): parameters = { @@ -52,7 +49,7 @@ class MercurialServerCreator(object): 'user_permissions': { 'test_hg': 'repository.admin' }, - 'config': self.config_mock, + 'settings': self.config_data['app:main'], 'env': plain_dummy_env() } parameters.update(kwargs) diff --git a/rhodecode/apps/ssh_support/tests/test_server_svn.py b/rhodecode/apps/ssh_support/tests/test_server_svn.py index 7102f801..52bd6ad1 100644 --- a/rhodecode/apps/ssh_support/tests/test_server_svn.py +++ b/rhodecode/apps/ssh_support/tests/test_server_svn.py @@ -36,10 +36,7 @@ class SubversionServerCreator(object): user = plain_dummy_user() def __init__(self): - def config_get(part, key): - return self.config_data.get(part, {}).get(key) - self.config_mock = mock.Mock() - self.config_mock.get = mock.Mock(side_effect=config_get) + pass def create(self, **kwargs): parameters = { @@ -50,7 +47,7 @@ class SubversionServerCreator(object): 'user_permissions': { self.repo_name: 'repository.admin' }, - 'config': self.config_mock, + 'settings': self.config_data['app:main'], 'env': plain_dummy_env() } diff --git a/rhodecode/apps/ssh_support/tests/test_ssh_wrapper.py b/rhodecode/apps/ssh_support/tests/test_ssh_wrapper.py index b5b7435b..fcb5aa93 100644 --- a/rhodecode/apps/ssh_support/tests/test_ssh_wrapper.py +++ b/rhodecode/apps/ssh_support/tests/test_ssh_wrapper.py @@ -28,10 +28,6 @@ class TestSSHWrapper(object): permissions={}, branch_permissions={}) assert str(exc_info.value) == 'Unrecognised VCS: microsoft-tfs' - def test_parse_config(self, ssh_wrapper): - config = ssh_wrapper.parse_config(ssh_wrapper.ini_path) - assert config - def test_get_connection_info(self, ssh_wrapper): conn_info = ssh_wrapper.get_connection_info() assert {'client_ip': '127.0.0.1', diff --git a/rhodecode/config/config_maker.py b/rhodecode/config/config_maker.py new file mode 100644 index 00000000..f33fd929 --- /dev/null +++ b/rhodecode/config/config_maker.py @@ -0,0 +1,198 @@ +# Copyright (C) 2010-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import os +import tempfile +import logging + +from pyramid.settings import asbool + +from rhodecode.config.settings_maker import SettingsMaker +from rhodecode.config import utils as config_utils + +log = logging.getLogger(__name__) + + +def sanitize_settings_and_apply_defaults(global_config, settings): + """ + Applies settings defaults and does all type conversion. + + We would move all settings parsing and preparation into this place, so that + we have only one place left which deals with this part. The remaining parts + of the application would start to rely fully on well-prepared settings. + + This piece would later be split up per topic to avoid a big fat monster + function. + """ + jn = os.path.join + + global_settings_maker = SettingsMaker(global_config) + global_settings_maker.make_setting('debug', default=False, parser='bool') + debug_enabled = asbool(global_config.get('debug')) + + settings_maker = SettingsMaker(settings) + + settings_maker.make_setting( + 'logging.autoconfigure', + default=False, + parser='bool') + + logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini') + settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG') + + # Default includes, possible to change as a user + pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline') + log.debug( + "Using the following pyramid.includes: %s", + pyramid_includes) + + settings_maker.make_setting('rhodecode.edition', 'Community Edition') + settings_maker.make_setting('rhodecode.edition_id', 'CE') + + if 'mako.default_filters' not in settings: + # set custom default filters if we don't have it defined + settings['mako.imports'] = 'from rhodecode.lib.base import h_filter' + settings['mako.default_filters'] = 'h_filter' + + if 'mako.directories' not in settings: + mako_directories = settings.setdefault('mako.directories', [ + # Base templates of the original application + 'rhodecode:templates', + ]) + log.debug( + "Using the following Mako template directories: %s", + mako_directories) + + # NOTE(marcink): fix redis requirement for schema of connection since 3.X + if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis': + raw_url = settings['beaker.session.url'] + if not raw_url.startswith(('redis://', 'rediss://', 'unix://')): + settings['beaker.session.url'] = 'redis://' + raw_url + + settings_maker.make_setting('__file__', global_config.get('__file__')) + + # TODO: johbo: Re-think this, usually the call to config.include + # should allow to pass in a prefix. + settings_maker.make_setting('rhodecode.api.url', '/_admin/api') + + # Sanitize generic settings. + settings_maker.make_setting('default_encoding', 'UTF-8', parser='list') + settings_maker.make_setting('is_test', False, parser='bool') + settings_maker.make_setting('gzip_responses', False, parser='bool') + + # statsd + settings_maker.make_setting('statsd.enabled', False, parser='bool') + settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string') + settings_maker.make_setting('statsd.statsd_port', 9125, parser='int') + settings_maker.make_setting('statsd.statsd_prefix', '') + settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool') + + settings_maker.make_setting('vcs.svn.compatible_version', '') + settings_maker.make_setting('vcs.hooks.protocol', 'http') + settings_maker.make_setting('vcs.hooks.host', '*') + settings_maker.make_setting('vcs.scm_app_implementation', 'http') + settings_maker.make_setting('vcs.server', '') + settings_maker.make_setting('vcs.server.protocol', 'http') + settings_maker.make_setting('vcs.server.enable', 'true', parser='bool') + settings_maker.make_setting('startup.import_repos', 'false', parser='bool') + settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool') + settings_maker.make_setting('vcs.start_server', 'false', parser='bool') + settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list') + settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int') + + settings_maker.make_setting('vcs.methods.cache', True, parser='bool') + + # Support legacy values of vcs.scm_app_implementation. Legacy + # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or + # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'. + scm_app_impl = settings['vcs.scm_app_implementation'] + if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']: + settings['vcs.scm_app_implementation'] = 'http' + + settings_maker.make_setting('appenlight', False, parser='bool') + + temp_store = tempfile.gettempdir() + tmp_cache_dir = jn(temp_store, 'rc_cache') + + # save default, cache dir, and use it for all backends later. + default_cache_dir = settings_maker.make_setting( + 'cache_dir', + default=tmp_cache_dir, default_when_empty=True, + parser='dir:ensured') + + # exception store cache + settings_maker.make_setting( + 'exception_tracker.store_path', + default=jn(default_cache_dir, 'exc_store'), default_when_empty=True, + parser='dir:ensured' + ) + + settings_maker.make_setting( + 'celerybeat-schedule.path', + default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True, + parser='file:ensured' + ) + + settings_maker.make_setting('exception_tracker.send_email', False, parser='bool') + settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True) + + # sessions, ensure file since no-value is memory + settings_maker.make_setting('beaker.session.type', 'file') + settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data')) + + # cache_general + settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace') + settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int') + settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db')) + + # cache_perms + settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace') + settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int') + settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db')) + + # cache_repo + settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace') + settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int') + settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db')) + + # cache_license + settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace') + settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int') + settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db')) + + # cache_repo_longterm memory, 96H + settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru') + settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int') + settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int') + + # sql_cache_short + settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru') + settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int') + settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int') + + # archive_cache + settings_maker.make_setting('archive_cache.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,) + settings_maker.make_setting('archive_cache.cache_size_gb', 10, parser='float') + settings_maker.make_setting('archive_cache.cache_shards', 10, parser='int') + + settings_maker.env_expand() + + # configure instance id + config_utils.set_instance_id(settings) + + return settings diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py index aa78fcf7..dafee2dc 100644 --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -19,7 +19,7 @@ import os import sys import collections -import tempfile + import time import logging.config @@ -32,14 +32,13 @@ from pyramid.httpexceptions import ( HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound) from pyramid.renderers import render_to_response -from rhodecode import api from rhodecode.model import meta from rhodecode.config import patches -from rhodecode.config import utils as config_utils -from rhodecode.config.settings_maker import SettingsMaker + from rhodecode.config.environment import load_pyramid_environment import rhodecode.events +from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults from rhodecode.lib.middleware.vcs import VCSMiddleware from rhodecode.lib.request import Request from rhodecode.lib.vcs import VCSCommunicationError @@ -465,173 +464,3 @@ def wrap_app_in_wsgi_middlewares(pyramid_app, config): log.debug('Request processing finalized: %.4fs', total) return pyramid_app_with_cleanup - - -def sanitize_settings_and_apply_defaults(global_config, settings): - """ - Applies settings defaults and does all type conversion. - - We would move all settings parsing and preparation into this place, so that - we have only one place left which deals with this part. The remaining parts - of the application would start to rely fully on well prepared settings. - - This piece would later be split up per topic to avoid a big fat monster - function. - """ - jn = os.path.join - - global_settings_maker = SettingsMaker(global_config) - global_settings_maker.make_setting('debug', default=False, parser='bool') - debug_enabled = asbool(global_config.get('debug')) - - settings_maker = SettingsMaker(settings) - - settings_maker.make_setting( - 'logging.autoconfigure', - default=False, - parser='bool') - - logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini') - settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG') - - # Default includes, possible to change as a user - pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline') - log.debug( - "Using the following pyramid.includes: %s", - pyramid_includes) - - settings_maker.make_setting('rhodecode.edition', 'Community Edition') - settings_maker.make_setting('rhodecode.edition_id', 'CE') - - if 'mako.default_filters' not in settings: - # set custom default filters if we don't have it defined - settings['mako.imports'] = 'from rhodecode.lib.base import h_filter' - settings['mako.default_filters'] = 'h_filter' - - if 'mako.directories' not in settings: - mako_directories = settings.setdefault('mako.directories', [ - # Base templates of the original application - 'rhodecode:templates', - ]) - log.debug( - "Using the following Mako template directories: %s", - mako_directories) - - # NOTE(marcink): fix redis requirement for schema of connection since 3.X - if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis': - raw_url = settings['beaker.session.url'] - if not raw_url.startswith(('redis://', 'rediss://', 'unix://')): - settings['beaker.session.url'] = 'redis://' + raw_url - - settings_maker.make_setting('__file__', global_config.get('__file__')) - - # TODO: johbo: Re-think this, usually the call to config.include - # should allow to pass in a prefix. - settings_maker.make_setting('rhodecode.api.url', api.DEFAULT_URL) - - # Sanitize generic settings. - settings_maker.make_setting('default_encoding', 'UTF-8', parser='list') - settings_maker.make_setting('is_test', False, parser='bool') - settings_maker.make_setting('gzip_responses', False, parser='bool') - - # statsd - settings_maker.make_setting('statsd.enabled', False, parser='bool') - settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string') - settings_maker.make_setting('statsd.statsd_port', 9125, parser='int') - settings_maker.make_setting('statsd.statsd_prefix', '') - settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool') - - settings_maker.make_setting('vcs.svn.compatible_version', '') - settings_maker.make_setting('vcs.hooks.protocol', 'http') - settings_maker.make_setting('vcs.hooks.host', '*') - settings_maker.make_setting('vcs.scm_app_implementation', 'http') - settings_maker.make_setting('vcs.server', '') - settings_maker.make_setting('vcs.server.protocol', 'http') - settings_maker.make_setting('vcs.server.enable', 'true', parser='bool') - settings_maker.make_setting('startup.import_repos', 'false', parser='bool') - settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool') - settings_maker.make_setting('vcs.start_server', 'false', parser='bool') - settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list') - settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int') - - settings_maker.make_setting('vcs.methods.cache', True, parser='bool') - - # Support legacy values of vcs.scm_app_implementation. Legacy - # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or - # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'. - scm_app_impl = settings['vcs.scm_app_implementation'] - if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']: - settings['vcs.scm_app_implementation'] = 'http' - - settings_maker.make_setting('appenlight', False, parser='bool') - - temp_store = tempfile.gettempdir() - tmp_cache_dir = jn(temp_store, 'rc_cache') - - # save default, cache dir, and use it for all backends later. - default_cache_dir = settings_maker.make_setting( - 'cache_dir', - default=tmp_cache_dir, default_when_empty=True, - parser='dir:ensured') - - # exception store cache - settings_maker.make_setting( - 'exception_tracker.store_path', - default=jn(default_cache_dir, 'exc_store'), default_when_empty=True, - parser='dir:ensured' - ) - - settings_maker.make_setting( - 'celerybeat-schedule.path', - default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True, - parser='file:ensured' - ) - - settings_maker.make_setting('exception_tracker.send_email', False, parser='bool') - settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True) - - # sessions, ensure file since no-value is memory - settings_maker.make_setting('beaker.session.type', 'file') - settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data')) - - # cache_general - settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace') - settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int') - settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db')) - - # cache_perms - settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace') - settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int') - settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db')) - - # cache_repo - settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace') - settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int') - settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db')) - - # cache_license - settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace') - settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int') - settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db')) - - # cache_repo_longterm memory, 96H - settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru') - settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int') - settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int') - - # sql_cache_short - settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru') - settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int') - settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int') - - # archive_cache - settings_maker.make_setting('archive_cache.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,) - settings_maker.make_setting('archive_cache.cache_size_gb', 10, parser='float') - settings_maker.make_setting('archive_cache.cache_shards', 10, parser='int') - - settings_maker.env_expand() - - # configure instance id - config_utils.set_instance_id(settings) - - return settings diff --git a/rhodecode/config/utils.py b/rhodecode/config/utils.py index 40c8d2f4..6f443954 100644 --- a/rhodecode/config/utils.py +++ b/rhodecode/config/utils.py @@ -19,8 +19,6 @@ import os import platform -from rhodecode.model import init_model - def configure_vcs(config): """ @@ -44,6 +42,7 @@ def configure_vcs(config): def initialize_database(config): from rhodecode.lib.utils2 import engine_from_config, get_encryption_key + from rhodecode.model import init_model engine = engine_from_config(config, 'sqlalchemy.db1.') init_model(engine, encryption_key=get_encryption_key(config)) diff --git a/rhodecode/lib/api_utils.py b/rhodecode/lib/api_utils.py new file mode 100644 index 00000000..6c8f11ad --- /dev/null +++ b/rhodecode/lib/api_utils.py @@ -0,0 +1,38 @@ +# Copyright (C) 2010-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import urllib.parse + +from rhodecode.lib.vcs import CurlSession +from rhodecode.lib.ext_json import json + + +def call_service_api(service_api_host, service_api_token, api_url, payload): + + payload.update({ + 'id': 'service', + 'auth_token': service_api_token + }) + + service_api_url = urllib.parse.urljoin(service_api_host, api_url) + response = CurlSession().post(service_api_url, json.dumps(payload)) + + if response.status_code != 200: + raise Exception(f"Service API at {service_api_url} responded with error: {response.status_code}") + + return json.loads(response.content)['result'] diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py index 7bd1c7d6..da8104fe 100644 --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -567,7 +567,7 @@ def add_events_routes(config): def bootstrap_config(request, registry_name='RcTestRegistry'): - from rhodecode.config.middleware import sanitize_settings_and_apply_defaults + from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults import pyramid.testing registry = pyramid.testing.Registry(registry_name) diff --git a/rhodecode/lib/config_utils.py b/rhodecode/lib/config_utils.py new file mode 100644 index 00000000..eeafef5e --- /dev/null +++ b/rhodecode/lib/config_utils.py @@ -0,0 +1,40 @@ +# Copyright (C) 2010-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ +import os + + +def get_config(ini_path, **kwargs): + import configparser + parser = configparser.ConfigParser(**kwargs) + parser.read(ini_path) + return parser + + +def get_app_config_lightweight(ini_path): + parser = get_config(ini_path) + parser.set('app:main', 'here', os.getcwd()) + parser.set('app:main', '__file__', ini_path) + return dict(parser.items('app:main')) + + +def get_app_config(ini_path): + """ + This loads the app context and provides a heavy type iniliaziation of config + """ + from paste.deploy.loadwsgi import appconfig + return appconfig(f'config:{ini_path}', relative_to=os.getcwd()) diff --git a/rhodecode/lib/hook_daemon/__init__.py b/rhodecode/lib/hook_daemon/__init__.py new file mode 100644 index 00000000..c4d86aeb --- /dev/null +++ b/rhodecode/lib/hook_daemon/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2010-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ diff --git a/rhodecode/lib/hook_daemon/base.py b/rhodecode/lib/hook_daemon/base.py new file mode 100644 index 00000000..14faca6d --- /dev/null +++ b/rhodecode/lib/hook_daemon/base.py @@ -0,0 +1,115 @@ +# Copyright (C) 2010-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ +import os +import time +import logging +import tempfile + +from rhodecode.lib.config_utils import get_config +from rhodecode.lib.ext_json import json + +log = logging.getLogger(__name__) + + +class BaseHooksCallbackDaemon: + """ + Basic context manager for actions that don't require some extra + """ + def __init__(self): + pass + + def __enter__(self): + log.debug('Running `%s` callback daemon', self.__class__.__name__) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + log.debug('Exiting `%s` callback daemon', self.__class__.__name__) + + +class HooksModuleCallbackDaemon(BaseHooksCallbackDaemon): + + def __init__(self, module): + super().__init__() + self.hooks_module = module + + +def get_txn_id_data_path(txn_id): + import rhodecode + + root = rhodecode.CONFIG.get('cache_dir') or tempfile.gettempdir() + final_dir = os.path.join(root, 'svn_txn_id') + + if not os.path.isdir(final_dir): + os.makedirs(final_dir) + return os.path.join(final_dir, 'rc_txn_id_{}'.format(txn_id)) + + +def store_txn_id_data(txn_id, data_dict): + if not txn_id: + log.warning('Cannot store txn_id because it is empty') + return + + path = get_txn_id_data_path(txn_id) + try: + with open(path, 'wb') as f: + f.write(json.dumps(data_dict)) + except Exception: + log.exception('Failed to write txn_id metadata') + + +def get_txn_id_from_store(txn_id): + """ + Reads txn_id from store and if present returns the data for callback manager + """ + path = get_txn_id_data_path(txn_id) + try: + with open(path, 'rb') as f: + return json.loads(f.read()) + except Exception: + return {} + + +def prepare_callback_daemon(extras, protocol, host, txn_id=None): + txn_details = get_txn_id_from_store(txn_id) + port = txn_details.get('port', 0) + match protocol: + case 'http': + from rhodecode.lib.hook_daemon.http_hooks_deamon import HttpHooksCallbackDaemon + callback_daemon = HttpHooksCallbackDaemon( + txn_id=txn_id, host=host, port=port) + case 'celery': + from rhodecode.lib.hook_daemon.celery_hooks_deamon import CeleryHooksCallbackDaemon + callback_daemon = CeleryHooksCallbackDaemon(get_config(extras['config'])) + case 'local': + from rhodecode.lib.hook_daemon.hook_module import Hooks + callback_daemon = HooksModuleCallbackDaemon(Hooks.__module__) + case _: + log.error('Unsupported callback daemon protocol "%s"', protocol) + raise Exception('Unsupported callback daemon protocol.') + + extras['hooks_uri'] = getattr(callback_daemon, 'hooks_uri', '') + extras['task_queue'] = getattr(callback_daemon, 'task_queue', '') + extras['task_backend'] = getattr(callback_daemon, 'task_backend', '') + extras['hooks_protocol'] = protocol + extras['time'] = time.time() + + # register txn_id + extras['txn_id'] = txn_id + log.debug('Prepared a callback daemon: %s', + callback_daemon.__class__.__name__) + return callback_daemon, extras diff --git a/rhodecode/lib/hook_daemon/celery_hooks_deamon.py b/rhodecode/lib/hook_daemon/celery_hooks_deamon.py new file mode 100644 index 00000000..fc9883f4 --- /dev/null +++ b/rhodecode/lib/hook_daemon/celery_hooks_deamon.py @@ -0,0 +1,30 @@ +# Copyright (C) 2010-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +from rhodecode.lib.hook_daemon.base import BaseHooksCallbackDaemon + + +class CeleryHooksCallbackDaemon(BaseHooksCallbackDaemon): + """ + Context manger for achieving a compatibility with celery backend + """ + + def __init__(self, config): + # TODO: replace this with settings bootstrapped... + self.task_queue = config.get('app:main', 'celery.broker_url') + self.task_backend = config.get('app:main', 'celery.result_backend') diff --git a/rhodecode/lib/hook_daemon/hook_module.py b/rhodecode/lib/hook_daemon/hook_module.py new file mode 100644 index 00000000..d719f88a --- /dev/null +++ b/rhodecode/lib/hook_daemon/hook_module.py @@ -0,0 +1,104 @@ +# Copyright (C) 2010-2023 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging +import traceback + +from rhodecode.model import meta + +from rhodecode.lib import hooks_base +from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected +from rhodecode.lib.utils2 import AttributeDict + +log = logging.getLogger(__name__) + + +class Hooks(object): + """ + Exposes the hooks for remote callbacks + """ + def __init__(self, request=None, log_prefix=''): + self.log_prefix = log_prefix + self.request = request + + def repo_size(self, extras): + log.debug("%sCalled repo_size of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.repo_size, extras) + + def pre_pull(self, extras): + log.debug("%sCalled pre_pull of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.pre_pull, extras) + + def post_pull(self, extras): + log.debug("%sCalled post_pull of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.post_pull, extras) + + def pre_push(self, extras): + log.debug("%sCalled pre_push of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.pre_push, extras) + + def post_push(self, extras): + log.debug("%sCalled post_push of %s object", self.log_prefix, self) + return self._call_hook(hooks_base.post_push, extras) + + def _call_hook(self, hook, extras): + extras = AttributeDict(extras) + _server_url = extras['server_url'] + + extras.request = self.request + + try: + result = hook(extras) + if result is None: + raise Exception(f'Failed to obtain hook result from func: {hook}') + except HTTPBranchProtected as handled_error: + # Those special cases don't need error reporting. It's a case of + # locked repo or protected branch + result = AttributeDict({ + 'status': handled_error.code, + 'output': handled_error.explanation + }) + except (HTTPLockedRC, Exception) as error: + # locked needs different handling since we need to also + # handle PULL operations + exc_tb = '' + if not isinstance(error, HTTPLockedRC): + exc_tb = traceback.format_exc() + log.exception('%sException when handling hook %s', self.log_prefix, hook) + error_args = error.args + return { + 'status': 128, + 'output': '', + 'exception': type(error).__name__, + 'exception_traceback': exc_tb, + 'exception_args': error_args, + } + finally: + meta.Session.remove() + + log.debug('%sGot hook call response %s', self.log_prefix, result) + return { + 'status': result.status, + 'output': result.output, + } + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass diff --git a/rhodecode/lib/hooks_daemon.py b/rhodecode/lib/hook_daemon/http_hooks_deamon.py similarity index 61% rename from rhodecode/lib/hooks_daemon.py rename to rhodecode/lib/hook_daemon/http_hooks_deamon.py index 2811e78f..73ecd0ff 100644 --- a/rhodecode/lib/hooks_daemon.py +++ b/rhodecode/lib/hook_daemon/http_hooks_deamon.py @@ -17,9 +17,7 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import os -import time import logging -import tempfile import traceback import threading import socket @@ -29,14 +27,11 @@ import gevent from http.server import BaseHTTPRequestHandler from socketserver import TCPServer -import rhodecode -from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected from rhodecode.model import meta -from rhodecode.lib import hooks_base -from rhodecode.lib.utils2 import AttributeDict -from rhodecode.lib.pyramid_utils import get_config from rhodecode.lib.ext_json import json from rhodecode.lib import rc_cache +from rhodecode.lib.hook_daemon.base import get_txn_id_data_path +from rhodecode.lib.hook_daemon.hook_module import Hooks log = logging.getLogger(__name__) @@ -126,7 +121,7 @@ class HooksHttpHandler(BaseHTTPRequestHandler): def log_message(self, format, *args): """ This is an overridden method of BaseHTTPRequestHandler which logs using - logging library instead of writing directly to stderr. + a logging library instead of writing directly to stderr. """ message = format % args @@ -136,31 +131,6 @@ class HooksHttpHandler(BaseHTTPRequestHandler): self.log_date_time_string(), message) -class BaseHooksCallbackDaemon: - """ - Basic context manager for actions that don't require some extra - """ - def __init__(self): - self.hooks_module = Hooks.__module__ - - def __enter__(self): - log.debug('Running `%s` callback daemon', self.__class__.__name__) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - log.debug('Exiting `%s` callback daemon', self.__class__.__name__) - - -class CeleryHooksCallbackDaemon(BaseHooksCallbackDaemon): - """ - Context manger for achieving a compatibility with celery backend - """ - - def __init__(self, config): - self.task_queue = config.get('app:main', 'celery.broker_url') - self.task_backend = config.get('app:main', 'celery.result_backend') - - class ThreadedHookCallbackDaemon(object): _callback_thread = None @@ -308,144 +278,3 @@ class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon): os.remove(txn_id_file) log.debug("Background greenlet done.") - - -def get_txn_id_data_path(txn_id): - import rhodecode - - root = rhodecode.CONFIG.get('cache_dir') or tempfile.gettempdir() - final_dir = os.path.join(root, 'svn_txn_id') - - if not os.path.isdir(final_dir): - os.makedirs(final_dir) - return os.path.join(final_dir, 'rc_txn_id_{}'.format(txn_id)) - - -def store_txn_id_data(txn_id, data_dict): - if not txn_id: - log.warning('Cannot store txn_id because it is empty') - return - - path = get_txn_id_data_path(txn_id) - try: - with open(path, 'wb') as f: - f.write(json.dumps(data_dict)) - except Exception: - log.exception('Failed to write txn_id metadata') - - -def get_txn_id_from_store(txn_id): - """ - Reads txn_id from store and if present returns the data for callback manager - """ - path = get_txn_id_data_path(txn_id) - try: - with open(path, 'rb') as f: - return json.loads(f.read()) - except Exception: - return {} - - -def prepare_callback_daemon(extras, protocol, host, txn_id=None): - txn_details = get_txn_id_from_store(txn_id) - port = txn_details.get('port', 0) - match protocol: - case 'http': - callback_daemon = HttpHooksCallbackDaemon( - txn_id=txn_id, host=host, port=port) - case 'celery': - callback_daemon = CeleryHooksCallbackDaemon(get_config(extras['config'])) - case 'local': - callback_daemon = BaseHooksCallbackDaemon() - case _: - log.error('Unsupported callback daemon protocol "%s"', protocol) - raise Exception('Unsupported callback daemon protocol.') - - extras['hooks_uri'] = getattr(callback_daemon, 'hooks_uri', '') - extras['task_queue'] = getattr(callback_daemon, 'task_queue', '') - extras['task_backend'] = getattr(callback_daemon, 'task_backend', '') - extras['hooks_protocol'] = protocol - extras['time'] = time.time() - - # register txn_id - extras['txn_id'] = txn_id - log.debug('Prepared a callback daemon: %s', - callback_daemon.__class__.__name__) - return callback_daemon, extras - - -class Hooks(object): - """ - Exposes the hooks for remote call backs - """ - def __init__(self, request=None, log_prefix=''): - self.log_prefix = log_prefix - self.request = request - - def repo_size(self, extras): - log.debug("%sCalled repo_size of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.repo_size, extras) - - def pre_pull(self, extras): - log.debug("%sCalled pre_pull of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.pre_pull, extras) - - def post_pull(self, extras): - log.debug("%sCalled post_pull of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.post_pull, extras) - - def pre_push(self, extras): - log.debug("%sCalled pre_push of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.pre_push, extras) - - def post_push(self, extras): - log.debug("%sCalled post_push of %s object", self.log_prefix, self) - return self._call_hook(hooks_base.post_push, extras) - - def _call_hook(self, hook, extras): - extras = AttributeDict(extras) - server_url = extras['server_url'] - - extras.request = self.request - - try: - result = hook(extras) - if result is None: - raise Exception( - 'Failed to obtain hook result from func: {}'.format(hook)) - except HTTPBranchProtected as handled_error: - # Those special cases doesn't need error reporting. It's a case of - # locked repo or protected branch - result = AttributeDict({ - 'status': handled_error.code, - 'output': handled_error.explanation - }) - except (HTTPLockedRC, Exception) as error: - # locked needs different handling since we need to also - # handle PULL operations - exc_tb = '' - if not isinstance(error, HTTPLockedRC): - exc_tb = traceback.format_exc() - log.exception('%sException when handling hook %s', self.log_prefix, hook) - error_args = error.args - return { - 'status': 128, - 'output': '', - 'exception': type(error).__name__, - 'exception_traceback': exc_tb, - 'exception_args': error_args, - } - finally: - meta.Session.remove() - - log.debug('%sGot hook call response %s', self.log_prefix, result) - return { - 'status': result.status, - 'output': result.output, - } - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - pass diff --git a/rhodecode/lib/middleware/simplesvn.py b/rhodecode/lib/middleware/simplesvn.py index 451e1708..681992b0 100644 --- a/rhodecode/lib/middleware/simplesvn.py +++ b/rhodecode/lib/middleware/simplesvn.py @@ -34,8 +34,7 @@ from rhodecode.lib.utils import is_valid_repo from rhodecode.lib.str_utils import safe_str, safe_int, safe_bytes from rhodecode.lib.type_utils import str2bool from rhodecode.lib.ext_json import json -from rhodecode.lib.hooks_daemon import store_txn_id_data - +from rhodecode.lib.hook_daemon.base import store_txn_id_data log = logging.getLogger(__name__) diff --git a/rhodecode/lib/middleware/simplevcs.py b/rhodecode/lib/middleware/simplevcs.py index 23e47104..57415de9 100644 --- a/rhodecode/lib/middleware/simplevcs.py +++ b/rhodecode/lib/middleware/simplevcs.py @@ -45,7 +45,7 @@ from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware from rhodecode.lib.base import ( BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context) from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError) -from rhodecode.lib.hooks_daemon import prepare_callback_daemon +from rhodecode.lib.hook_daemon.base import prepare_callback_daemon from rhodecode.lib.middleware import appenlight from rhodecode.lib.middleware.utils import scm_app_http from rhodecode.lib.str_utils import safe_bytes diff --git a/rhodecode/lib/pyramid_utils.py b/rhodecode/lib/pyramid_utils.py index 81808f21..51aa1816 100644 --- a/rhodecode/lib/pyramid_utils.py +++ b/rhodecode/lib/pyramid_utils.py @@ -20,24 +20,14 @@ import os import configparser + +from rhodecode.lib.config_utils import get_config from pyramid.paster import bootstrap as pyramid_bootstrap, setup_logging # pragma: no cover -from rhodecode.lib.request import Request - - -def get_config(ini_path, **kwargs): - parser = configparser.ConfigParser(**kwargs) - parser.read(ini_path) - return parser - - -def get_app_config(ini_path): - from paste.deploy.loadwsgi import appconfig - return appconfig(f'config:{ini_path}', relative_to=os.getcwd()) - def bootstrap(config_uri, options=None, env=None): from rhodecode.lib.utils2 import AttributeDict + from rhodecode.lib.request import Request if env: os.environ.update(env) diff --git a/rhodecode/lib/rc_commands/setup_rc.py b/rhodecode/lib/rc_commands/setup_rc.py index 82427e44..b5626f6f 100644 --- a/rhodecode/lib/rc_commands/setup_rc.py +++ b/rhodecode/lib/rc_commands/setup_rc.py @@ -20,7 +20,8 @@ import logging import click import pyramid.paster -from rhodecode.lib.pyramid_utils import bootstrap, get_app_config +from rhodecode.lib.pyramid_utils import bootstrap +from rhodecode.lib.config_utils import get_app_config from rhodecode.lib.db_manage import DbManage from rhodecode.lib.utils2 import get_encryption_key from rhodecode.model.db import Session diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py index 01902210..d331d7cd 100644 --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -32,11 +32,9 @@ import socket import tempfile import traceback import tarfile -import urllib.parse -import warnings + from functools import wraps from os.path import join as jn -from configparser import NoOptionError import paste import pkg_resources @@ -55,9 +53,6 @@ from rhodecode.model import meta from rhodecode.model.db import ( Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup) from rhodecode.model.meta import Session -from rhodecode.lib.pyramid_utils import get_config -from rhodecode.lib.vcs import CurlSession -from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError log = logging.getLogger(__name__) @@ -827,33 +822,3 @@ def send_test_email(recipients, email_body='TEST EMAIL'): email_body = email_body_plaintext = email_body subject = f'SUBJECT FROM: {socket.gethostname()}' tasks.send_email(recipients, subject, email_body_plaintext, email_body) - - -def call_service_api(ini_path, payload): - config = get_config(ini_path) - try: - host = config.get('app:main', 'app.service_api.host') - except NoOptionError: - raise ImproperlyConfiguredError( - "app.service_api.host is missing. " - "Please ensure that app.service_api.host and app.service_api.token are " - "defined inside of .ini configuration file." - ) - try: - api_url = config.get('app:main', 'rhodecode.api.url') - except NoOptionError: - from rhodecode import api - log.debug('Cannot find rhodecode.api.url, setting API URL TO Default value') - api_url = api.DEFAULT_URL - - payload.update({ - 'id': 'service', - 'auth_token': config.get('app:main', 'app.service_api.token') - }) - - response = CurlSession().post(urllib.parse.urljoin(host, api_url), json.dumps(payload)) - - if response.status_code != 200: - raise Exception("Service API responded with error") - - return json.loads(response.content)['result'] diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py index 420e00b6..0848a826 100644 --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -38,7 +38,7 @@ from rhodecode.translation import lazy_ugettext from rhodecode.lib import helpers as h, hooks_utils, diffs from rhodecode.lib import audit_logger from collections import OrderedDict -from rhodecode.lib.hooks_daemon import prepare_callback_daemon +from rhodecode.lib.hook_daemon.base import prepare_callback_daemon from rhodecode.lib.ext_json import sjson as json from rhodecode.lib.markup_renderer import ( DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer) diff --git a/rhodecode/tests/fixture_mods/fixture_pyramid.py b/rhodecode/tests/fixture_mods/fixture_pyramid.py index 4c3450cd..75a2fcea 100644 --- a/rhodecode/tests/fixture_mods/fixture_pyramid.py +++ b/rhodecode/tests/fixture_mods/fixture_pyramid.py @@ -19,7 +19,7 @@ import pytest -from rhodecode.lib.pyramid_utils import get_app_config +from rhodecode.lib.config_utils import get_app_config from rhodecode.tests.fixture import TestINI from rhodecode.tests.server_utils import RcVCSServer diff --git a/rhodecode/tests/fixture_mods/fixture_utils.py b/rhodecode/tests/fixture_mods/fixture_utils.py index 8d43a457..3351c104 100644 --- a/rhodecode/tests/fixture_mods/fixture_utils.py +++ b/rhodecode/tests/fixture_mods/fixture_utils.py @@ -174,7 +174,7 @@ def http_environ(): @pytest.fixture(scope='session') def baseapp(ini_config, vcsserver, http_environ_session): - from rhodecode.lib.pyramid_utils import get_app_config + from rhodecode.lib.config_utils import get_app_config from rhodecode.config.middleware import make_pyramid_app log.info("Using the RhodeCode configuration:{}".format(ini_config)) diff --git a/rhodecode/tests/lib/test_hooks_daemon.py b/rhodecode/tests/lib/test_hooks_daemon.py index 95832249..95e05e49 100644 --- a/rhodecode/tests/lib/test_hooks_daemon.py +++ b/rhodecode/tests/lib/test_hooks_daemon.py @@ -25,17 +25,20 @@ import msgpack import pytest import tempfile -from rhodecode.lib import hooks_daemon +from rhodecode.lib.hook_daemon import http_hooks_deamon +from rhodecode.lib.hook_daemon import celery_hooks_deamon +from rhodecode.lib.hook_daemon import hook_module +from rhodecode.lib.hook_daemon import base as hook_base from rhodecode.lib.str_utils import safe_bytes from rhodecode.tests.utils import assert_message_in_log from rhodecode.lib.ext_json import json -test_proto = hooks_daemon.HooksHttpHandler.MSGPACK_HOOKS_PROTO +test_proto = http_hooks_deamon.HooksHttpHandler.MSGPACK_HOOKS_PROTO class TestHooks(object): def test_hooks_can_be_used_as_a_context_processor(self): - hooks = hooks_daemon.Hooks() + hooks = hook_module.Hooks() with hooks as return_value: pass assert hooks == return_value @@ -52,10 +55,10 @@ class TestHooksHttpHandler(object): } request = self._generate_post_request(data) hooks_patcher = mock.patch.object( - hooks_daemon.Hooks, data['method'], create=True, return_value=1) + hook_module.Hooks, data['method'], create=True, return_value=1) with hooks_patcher as hooks_mock: - handler = hooks_daemon.HooksHttpHandler + handler = http_hooks_deamon.HooksHttpHandler handler.DEFAULT_HOOKS_PROTO = test_proto handler.wbufsize = 10240 MockServer(handler, request) @@ -73,21 +76,21 @@ class TestHooksHttpHandler(object): # patching our _read to return test method and proto used read_patcher = mock.patch.object( - hooks_daemon.HooksHttpHandler, '_read_request', + http_hooks_deamon.HooksHttpHandler, '_read_request', return_value=(test_proto, rpc_method, extras)) # patch Hooks instance to return hook_result data on 'test' call hooks_patcher = mock.patch.object( - hooks_daemon.Hooks, rpc_method, create=True, + hook_module.Hooks, rpc_method, create=True, return_value=hook_result) with read_patcher, hooks_patcher: - handler = hooks_daemon.HooksHttpHandler + handler = http_hooks_deamon.HooksHttpHandler handler.DEFAULT_HOOKS_PROTO = test_proto handler.wbufsize = 10240 server = MockServer(handler, request) - expected_result = hooks_daemon.HooksHttpHandler.serialize_data(hook_result) + expected_result = http_hooks_deamon.HooksHttpHandler.serialize_data(hook_result) server.request.output_stream.seek(0) assert server.request.output_stream.readlines()[-1] == expected_result @@ -97,15 +100,15 @@ class TestHooksHttpHandler(object): rpc_method = 'test' read_patcher = mock.patch.object( - hooks_daemon.HooksHttpHandler, '_read_request', + http_hooks_deamon.HooksHttpHandler, '_read_request', return_value=(test_proto, rpc_method, {})) hooks_patcher = mock.patch.object( - hooks_daemon.Hooks, rpc_method, create=True, + hook_module.Hooks, rpc_method, create=True, side_effect=Exception('Test exception')) with read_patcher, hooks_patcher: - handler = hooks_daemon.HooksHttpHandler + handler = http_hooks_deamon.HooksHttpHandler handler.DEFAULT_HOOKS_PROTO = test_proto handler.wbufsize = 10240 server = MockServer(handler, request) @@ -113,7 +116,7 @@ class TestHooksHttpHandler(object): server.request.output_stream.seek(0) data = server.request.output_stream.readlines() msgpack_data = b''.join(data[5:]) - org_exc = hooks_daemon.HooksHttpHandler.deserialize_data(msgpack_data) + org_exc = http_hooks_deamon.HooksHttpHandler.deserialize_data(msgpack_data) expected_result = { 'exception': 'Exception', 'exception_traceback': org_exc['exception_traceback'], @@ -123,8 +126,7 @@ class TestHooksHttpHandler(object): def test_log_message_writes_to_debug_log(self, caplog): ip_port = ('0.0.0.0', 8888) - handler = hooks_daemon.HooksHttpHandler( - MockRequest('POST /'), ip_port, mock.Mock()) + handler = http_hooks_deamon.HooksHttpHandler(MockRequest('POST /'), ip_port, mock.Mock()) fake_date = '1/Nov/2015 00:00:00' date_patcher = mock.patch.object( handler, 'log_date_time_string', return_value=fake_date) @@ -136,10 +138,10 @@ class TestHooksHttpHandler(object): assert_message_in_log( caplog.records, expected_message, - levelno=logging.DEBUG, module='hooks_daemon') + levelno=logging.DEBUG, module='http_hooks_deamon') def _generate_post_request(self, data, proto=test_proto): - if proto == hooks_daemon.HooksHttpHandler.MSGPACK_HOOKS_PROTO: + if proto == http_hooks_deamon.HooksHttpHandler.MSGPACK_HOOKS_PROTO: payload = msgpack.packb(data) else: payload = json.dumps(data) @@ -151,18 +153,18 @@ class TestHooksHttpHandler(object): class ThreadedHookCallbackDaemon(object): def test_constructor_calls_prepare(self): prepare_daemon_patcher = mock.patch.object( - hooks_daemon.ThreadedHookCallbackDaemon, '_prepare') + http_hooks_deamon.ThreadedHookCallbackDaemon, '_prepare') with prepare_daemon_patcher as prepare_daemon_mock: - hooks_daemon.ThreadedHookCallbackDaemon() + http_hooks_deamon.ThreadedHookCallbackDaemon() prepare_daemon_mock.assert_called_once_with() def test_run_is_called_on_context_start(self): patchers = mock.patch.multiple( - hooks_daemon.ThreadedHookCallbackDaemon, + http_hooks_deamon.ThreadedHookCallbackDaemon, _run=mock.DEFAULT, _prepare=mock.DEFAULT, __exit__=mock.DEFAULT) with patchers as mocks: - daemon = hooks_daemon.ThreadedHookCallbackDaemon() + daemon = http_hooks_deamon.ThreadedHookCallbackDaemon() with daemon as daemon_context: pass mocks['_run'].assert_called_once_with() @@ -170,11 +172,11 @@ class ThreadedHookCallbackDaemon(object): def test_stop_is_called_on_context_exit(self): patchers = mock.patch.multiple( - hooks_daemon.ThreadedHookCallbackDaemon, + http_hooks_deamon.ThreadedHookCallbackDaemon, _run=mock.DEFAULT, _prepare=mock.DEFAULT, _stop=mock.DEFAULT) with patchers as mocks: - daemon = hooks_daemon.ThreadedHookCallbackDaemon() + daemon = http_hooks_deamon.ThreadedHookCallbackDaemon() with daemon as daemon_context: assert mocks['_stop'].call_count == 0 @@ -185,46 +187,47 @@ class ThreadedHookCallbackDaemon(object): class TestHttpHooksCallbackDaemon(object): def test_hooks_callback_generates_new_port(self, caplog): with caplog.at_level(logging.DEBUG): - daemon = hooks_daemon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881) + daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881) assert daemon._daemon.server_address == ('127.0.0.1', 8881) with caplog.at_level(logging.DEBUG): - daemon = hooks_daemon.HttpHooksCallbackDaemon(host=None, port=None) + daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host=None, port=None) assert daemon._daemon.server_address[1] in range(0, 66000) assert daemon._daemon.server_address[0] != '127.0.0.1' def test_prepare_inits_daemon_variable(self, tcp_server, caplog): with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG): - daemon = hooks_daemon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881) + daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881) assert daemon._daemon == tcp_server _, port = tcp_server.server_address msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \ - f"hook object: " + f"hook object: " assert_message_in_log( - caplog.records, msg, levelno=logging.DEBUG, module='hooks_daemon') + caplog.records, msg, levelno=logging.DEBUG, module='http_hooks_deamon') def test_prepare_inits_hooks_uri_and_logs_it( self, tcp_server, caplog): with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG): - daemon = hooks_daemon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881) + daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881) _, port = tcp_server.server_address expected_uri = '{}:{}'.format('127.0.0.1', port) assert daemon.hooks_uri == expected_uri msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \ - f"hook object: " + f"hook object: " + assert_message_in_log( caplog.records, msg, - levelno=logging.DEBUG, module='hooks_daemon') + levelno=logging.DEBUG, module='http_hooks_deamon') def test_run_creates_a_thread(self, tcp_server): thread = mock.Mock() with self._tcp_patcher(tcp_server): - daemon = hooks_daemon.HttpHooksCallbackDaemon() + daemon = http_hooks_deamon.HttpHooksCallbackDaemon() with self._thread_patcher(thread) as thread_mock: daemon._run() @@ -238,7 +241,7 @@ class TestHttpHooksCallbackDaemon(object): def test_run_logs(self, tcp_server, caplog): with self._tcp_patcher(tcp_server): - daemon = hooks_daemon.HttpHooksCallbackDaemon() + daemon = http_hooks_deamon.HttpHooksCallbackDaemon() with self._thread_patcher(mock.Mock()), caplog.at_level(logging.DEBUG): daemon._run() @@ -246,13 +249,13 @@ class TestHttpHooksCallbackDaemon(object): assert_message_in_log( caplog.records, 'Running thread-based loop of callback daemon in background', - levelno=logging.DEBUG, module='hooks_daemon') + levelno=logging.DEBUG, module='http_hooks_deamon') def test_stop_cleans_up_the_connection(self, tcp_server, caplog): thread = mock.Mock() with self._tcp_patcher(tcp_server): - daemon = hooks_daemon.HttpHooksCallbackDaemon() + daemon = http_hooks_deamon.HttpHooksCallbackDaemon() with self._thread_patcher(thread), caplog.at_level(logging.DEBUG): with daemon: @@ -266,18 +269,19 @@ class TestHttpHooksCallbackDaemon(object): assert_message_in_log( caplog.records, 'Waiting for background thread to finish.', - levelno=logging.DEBUG, module='hooks_daemon') + levelno=logging.DEBUG, module='http_hooks_deamon') def _tcp_patcher(self, tcp_server): return mock.patch.object( - hooks_daemon, 'TCPServer', return_value=tcp_server) + http_hooks_deamon, 'TCPServer', return_value=tcp_server) def _thread_patcher(self, thread): return mock.patch.object( - hooks_daemon.threading, 'Thread', return_value=thread) + http_hooks_deamon.threading, 'Thread', return_value=thread) class TestPrepareHooksDaemon(object): + @pytest.mark.parametrize('protocol', ('celery',)) def test_returns_celery_hooks_callback_daemon_when_celery_protocol_specified( self, protocol): @@ -286,12 +290,12 @@ class TestPrepareHooksDaemon(object): "celery.result_backend = redis://redis/0") temp_file.flush() expected_extras = {'config': temp_file.name} - callback, extras = hooks_daemon.prepare_callback_daemon( + callback, extras = hook_base.prepare_callback_daemon( expected_extras, protocol=protocol, host='') - assert isinstance(callback, hooks_daemon.CeleryHooksCallbackDaemon) + assert isinstance(callback, celery_hooks_deamon.CeleryHooksCallbackDaemon) @pytest.mark.parametrize('protocol, expected_class', ( - ('http', hooks_daemon.HttpHooksCallbackDaemon), + ('http', http_hooks_deamon.HttpHooksCallbackDaemon), )) def test_returns_real_hooks_callback_daemon_when_protocol_is_specified( self, protocol, expected_class): @@ -302,7 +306,7 @@ class TestPrepareHooksDaemon(object): 'task_backend': '', 'task_queue': '' } - callback, extras = hooks_daemon.prepare_callback_daemon( + callback, extras = hook_base.prepare_callback_daemon( expected_extras.copy(), protocol=protocol, host='127.0.0.1', txn_id='txnid2') assert isinstance(callback, expected_class) @@ -321,7 +325,7 @@ class TestPrepareHooksDaemon(object): 'hooks_protocol': protocol.lower() } with pytest.raises(Exception): - callback, extras = hooks_daemon.prepare_callback_daemon( + callback, extras = hook_base.prepare_callback_daemon( expected_extras.copy(), protocol=protocol, host='127.0.0.1') From 51a688c0c018605940ac18317091b86432f141f3 Mon Sep 17 00:00:00 2001 From: Serhii Ilin Date: Wed, 28 Feb 2024 11:52:11 +0200 Subject: [PATCH 023/150] refactor(ssh-wrapper): changed SSHVcsServer to SshVcsServer, updated call_service_api method. --- .../apps/ssh_support/lib/backends/__init__.py | 34 ++----------------- .../apps/ssh_support/lib/backends/base.py | 8 ++--- .../apps/ssh_support/lib/backends/git.py | 4 +-- rhodecode/apps/ssh_support/lib/backends/hg.py | 9 ++--- .../apps/ssh_support/lib/backends/svn.py | 4 +-- rhodecode/lib/api_utils.py | 19 ++++++++--- 6 files changed, 25 insertions(+), 53 deletions(-) diff --git a/rhodecode/apps/ssh_support/lib/backends/__init__.py b/rhodecode/apps/ssh_support/lib/backends/__init__.py index f0a3e80e..01a7283e 100644 --- a/rhodecode/apps/ssh_support/lib/backends/__init__.py +++ b/rhodecode/apps/ssh_support/lib/backends/__init__.py @@ -24,7 +24,6 @@ from sqlalchemy import Table from rhodecode.lib.api_utils import call_service_api from rhodecode.lib.utils2 import AttributeDict -from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError from .hg import MercurialServer from .git import GitServer @@ -265,35 +264,6 @@ class SshWrapperStandalone(SshWrapper): New version of SshWrapper designed to be depended only on service API """ repos_path = None - service_api_host: str - service_api_token: str - api_url: str - - def __init__(self, command, connection_info, mode, - user, user_id, key_id: int, shell, ini_path: str, settings, env): - - # validate our settings for making a standalone calls - try: - self.service_api_host = settings['app.service_api.host'] - self.service_api_token = settings['app.service_api.token'] - except KeyError: - raise ImproperlyConfiguredError( - "app.service_api.host or app.service_api.token are missing. " - "Please ensure that app.service_api.host and app.service_api.token are " - "defined inside of .ini configuration file." - ) - - try: - self.api_url = settings['rhodecode.api.url'] - except KeyError: - raise ImproperlyConfiguredError( - "rhodecode.api.url is missing. " - "Please ensure that rhodecode.api.url is " - "defined inside of .ini configuration file." - ) - - super(SshWrapperStandalone, self).__init__( - command, connection_info, mode, user, user_id, key_id, shell, ini_path, settings, env) @staticmethod def parse_user_related_data(user_data): @@ -326,7 +296,7 @@ class SshWrapperStandalone(SshWrapper): exit_code = 1 elif scm_detected: - data = call_service_api(self.service_api_host, self.service_api_token, self.api_url, { + data = call_service_api(self.settings, { "method": "service_get_data_for_ssh_wrapper", "args": {"user_id": user_id, "repo_name": scm_repo, "key_id": self.key_id} }) @@ -364,7 +334,7 @@ class SshWrapperStandalone(SshWrapper): if repo_name.startswith('_'): org_repo_name = repo_name log.debug('translating UID repo %s', org_repo_name) - by_id_match = call_service_api(self.service_api_host, self.service_api_token, self.api_url, { + by_id_match = call_service_api(self.settings, { 'method': 'service_get_repo_name_by_id', "args": {"repo_id": repo_name} }) diff --git a/rhodecode/apps/ssh_support/lib/backends/base.py b/rhodecode/apps/ssh_support/lib/backends/base.py index 46ffe33d..ee1e2a78 100644 --- a/rhodecode/apps/ssh_support/lib/backends/base.py +++ b/rhodecode/apps/ssh_support/lib/backends/base.py @@ -28,7 +28,7 @@ from rhodecode.lib.api_utils import call_service_api log = logging.getLogger(__name__) -class SSHVcsServer(object): +class SshVcsServer(object): repo_user_agent = None # set in child classes _path = None # set executable path for hg/git/svn binary backend = None # set in child classes @@ -62,11 +62,7 @@ class SSHVcsServer(object): from rhodecode.model.scm import ScmModel ScmModel().mark_for_invalidation(repo_name) case 'celery': - service_api_host = self.settings['app.service_api.host'] - service_api_token = self.settings['app.service_api.token'] - api_url = self.settings['rhodecode.api.url'] - - call_service_api(service_api_host, service_api_token, api_url, { + call_service_api(self.settings, { "method": "service_mark_for_invalidation", "args": {"repo_name": repo_name} }) diff --git a/rhodecode/apps/ssh_support/lib/backends/git.py b/rhodecode/apps/ssh_support/lib/backends/git.py index ffedfb85..26dba531 100644 --- a/rhodecode/apps/ssh_support/lib/backends/git.py +++ b/rhodecode/apps/ssh_support/lib/backends/git.py @@ -21,7 +21,7 @@ import logging import subprocess from vcsserver import hooks -from .base import SSHVcsServer +from .base import SshVcsServer log = logging.getLogger(__name__) @@ -70,7 +70,7 @@ class GitTunnelWrapper(object): return result -class GitServer(SSHVcsServer): +class GitServer(SshVcsServer): backend = 'git' repo_user_agent = 'git' diff --git a/rhodecode/apps/ssh_support/lib/backends/hg.py b/rhodecode/apps/ssh_support/lib/backends/hg.py index e7f7ed80..3be6000e 100644 --- a/rhodecode/apps/ssh_support/lib/backends/hg.py +++ b/rhodecode/apps/ssh_support/lib/backends/hg.py @@ -23,7 +23,7 @@ import tempfile import textwrap import collections -from .base import SSHVcsServer +from .base import SshVcsServer from rhodecode.lib.api_utils import call_service_api @@ -94,7 +94,7 @@ class MercurialTunnelWrapper(object): self.remove_configs() -class MercurialServer(SSHVcsServer): +class MercurialServer(SshVcsServer): backend = 'hg' repo_user_agent = 'mercurial' cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks'] @@ -111,10 +111,7 @@ class MercurialServer(SSHVcsServer): def config_to_hgrc(self, repo_name): # Todo: once transition is done only call to service api should exist if self.hooks_protocol == 'celery': - service_api_host = self.settings['app.service_api.host'] - service_api_token = self.settings['app.service_api.token'] - api_url = self.settings['rhodecode.api.url'] - data = call_service_api(service_api_host, service_api_token, api_url, { + data = call_service_api(self.settings, { "method": "service_config_to_hgrc", "args": {"cli_flags": self.cli_flags, "repo_name": repo_name} }) diff --git a/rhodecode/apps/ssh_support/lib/backends/svn.py b/rhodecode/apps/ssh_support/lib/backends/svn.py index e7fefc29..031dabbd 100644 --- a/rhodecode/apps/ssh_support/lib/backends/svn.py +++ b/rhodecode/apps/ssh_support/lib/backends/svn.py @@ -25,7 +25,7 @@ import tempfile from subprocess import Popen, PIPE import urllib.parse -from .base import SSHVcsServer +from .base import SshVcsServer log = logging.getLogger(__name__) @@ -218,7 +218,7 @@ class SubversionTunnelWrapper(object): return self.return_code -class SubversionServer(SSHVcsServer): +class SubversionServer(SshVcsServer): backend = 'svn' repo_user_agent = 'svn' diff --git a/rhodecode/lib/api_utils.py b/rhodecode/lib/api_utils.py index 6c8f11ad..00e18c9f 100644 --- a/rhodecode/lib/api_utils.py +++ b/rhodecode/lib/api_utils.py @@ -20,16 +20,25 @@ import urllib.parse from rhodecode.lib.vcs import CurlSession from rhodecode.lib.ext_json import json +from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError -def call_service_api(service_api_host, service_api_token, api_url, payload): - +def call_service_api(settings, payload): + try: + api_host = settings['app.service_api.host'] + api_token = settings['app.service_api.token'] + api_url = settings['rhodecode.api.url'] + except KeyError as exc: + raise ImproperlyConfiguredError( + f"{str(exc)} is missing. " + "Please ensure that app.service_api.host, app.service_api.token and rhodecode.api.url are " + "defined inside of .ini configuration file." + ) payload.update({ 'id': 'service', - 'auth_token': service_api_token + 'auth_token': api_token }) - - service_api_url = urllib.parse.urljoin(service_api_host, api_url) + service_api_url = urllib.parse.urljoin(api_host, api_url) response = CurlSession().post(service_api_url, json.dumps(payload)) if response.status_code != 200: From 7e2e69d2ba09e8b2958d36efb293e553158e3df1 Mon Sep 17 00:00:00 2001 From: Serhii Ilin Date: Fri, 1 Mar 2024 09:39:14 +0200 Subject: [PATCH 024/150] feat(repo_path-config): moved main storage location path into ini file. Fixes: RCCE-61 --- configs/development.ini | 4 +-- configs/production.ini | 4 +-- rhodecode/api/views/server_api.py | 5 ++-- rhodecode/apps/admin/views/settings.py | 7 ++--- rhodecode/apps/svn_support/utils.py | 4 +-- rhodecode/config/config_maker.py | 2 ++ rhodecode/config/environment.py | 1 - rhodecode/config/utils.py | 12 -------- rhodecode/lib/base.py | 2 -- rhodecode/lib/middleware/simplevcs.py | 11 ++----- rhodecode/lib/system_info.py | 13 ++++---- rhodecode/lib/utils.py | 4 +-- rhodecode/model/__init__.py | 9 ++++++ rhodecode/model/db.py | 11 +++---- rhodecode/model/forms.py | 4 --- rhodecode/model/repo.py | 8 ----- rhodecode/model/repo_group.py | 9 ------ rhodecode/model/scm.py | 9 ------ rhodecode/model/settings.py | 3 -- rhodecode/subscribers.py | 5 ++-- .../admin/settings/settings_vcs.mako | 3 +- rhodecode/templates/base/vcs_settings.mako | 30 +------------------ rhodecode/tests/__init__.py | 8 +++-- .../tests/lib/middleware/test_simplesvn.py | 8 ++--- .../models/settings/test_vcs_settings.py | 15 ---------- rhodecode/tests/models/test_repos.py | 26 ++++++++-------- rhodecode/tests/rhodecode.ini | 3 -- 27 files changed, 63 insertions(+), 157 deletions(-) diff --git a/configs/development.ini b/configs/development.ini index d7a492a2..efbeaa4b 100644 --- a/configs/development.ini +++ b/configs/development.ini @@ -261,8 +261,8 @@ auth_ret_code_detection = false ; codes don't break the transactions while 4XX codes do lock_ret_code = 423 -; allows to change the repository location in settings page -allow_repo_location_change = true +; Filesystem location were repositories should be stored +repo_store.path = /var/opt/rhodecode_repo_store ; allows to setup custom hooks in settings page allow_custom_hooks_settings = true diff --git a/configs/production.ini b/configs/production.ini index 6ffeeb7d..cfe98212 100644 --- a/configs/production.ini +++ b/configs/production.ini @@ -212,8 +212,8 @@ auth_ret_code_detection = false ; codes don't break the transactions while 4XX codes do lock_ret_code = 423 -; allows to change the repository location in settings page -allow_repo_location_change = true +; Filesystem location were repositories should be stored +repo_store.path = /var/opt/rhodecode_repo_store ; allows to setup custom hooks in settings page allow_custom_hooks_settings = true diff --git a/rhodecode/api/views/server_api.py b/rhodecode/api/views/server_api.py index e4881dac..c93b55fe 100644 --- a/rhodecode/api/views/server_api.py +++ b/rhodecode/api/views/server_api.py @@ -25,7 +25,7 @@ from rhodecode.api import ( from rhodecode.api.utils import ( Optional, OAttr, has_superadmin_permission, get_user_or_error) -from rhodecode.lib.utils import repo2db_mapper +from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path from rhodecode.lib import system_info from rhodecode.lib import user_sessions from rhodecode.lib import exc_tracking @@ -33,7 +33,6 @@ from rhodecode.lib.ext_json import json from rhodecode.lib.utils2 import safe_int from rhodecode.model.db import UserIpMap from rhodecode.model.scm import ScmModel -from rhodecode.model.settings import VcsSettingsModel from rhodecode.apps.file_store import utils from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \ FileOverSizeException @@ -103,7 +102,7 @@ def get_repo_store(request, apiuser): if not has_superadmin_permission(apiuser): raise JSONRPCForbidden() - path = VcsSettingsModel().get_repos_location() + path = get_rhodecode_repo_store_path() return {"path": path} diff --git a/rhodecode/apps/admin/views/settings.py b/rhodecode/apps/admin/views/settings.py index 754cfa0f..5443da02 100644 --- a/rhodecode/apps/admin/views/settings.py +++ b/rhodecode/apps/admin/views/settings.py @@ -38,7 +38,7 @@ from rhodecode.lib.auth import ( LoginRequired, HasPermissionAllDecorator, CSRFRequired) from rhodecode.lib.celerylib import tasks, run_task from rhodecode.lib.str_utils import safe_str -from rhodecode.lib.utils import repo2db_mapper +from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path from rhodecode.lib.utils2 import str2bool, AttributeDict from rhodecode.lib.index import searcher_from_config @@ -167,9 +167,6 @@ class AdminSettingsView(BaseAppView): return Response(html) try: - if c.visual.allow_repo_location_change: - model.update_global_path_setting(form_result['paths_root_path']) - model.update_global_ssl_setting(form_result['web_push_ssl']) model.update_global_hook_settings(form_result) @@ -217,7 +214,7 @@ class AdminSettingsView(BaseAppView): def settings_mapping(self): c = self.load_default_context() c.active = 'mapping' - c.storage_path = VcsSettingsModel().get_repos_location() + c.storage_path = get_rhodecode_repo_store_path() data = render('rhodecode:templates/admin/settings/settings.mako', self._get_template_context(c), self.request) html = formencode.htmlfill.render( diff --git a/rhodecode/apps/svn_support/utils.py b/rhodecode/apps/svn_support/utils.py index 621e54ea..ec0d2a7f 100644 --- a/rhodecode/apps/svn_support/utils.py +++ b/rhodecode/apps/svn_support/utils.py @@ -22,7 +22,7 @@ import os from pyramid.renderers import render from rhodecode.events import trigger -from rhodecode.lib.utils import get_rhodecode_realm, get_rhodecode_base_path +from rhodecode.lib.utils import get_rhodecode_realm, get_rhodecode_repo_store_path from rhodecode.lib.utils2 import str2bool from rhodecode.model.db import RepoGroup @@ -38,7 +38,7 @@ def write_mod_dav_svn_config(settings): file_path = settings[config_keys.config_file_path] config = _render_mod_dav_svn_config( use_ssl=use_ssl, - parent_path_root=get_rhodecode_base_path(), + parent_path_root=get_rhodecode_repo_store_path(), list_parent_path=settings[config_keys.list_parent_path], location_root=settings[config_keys.location_root], repo_groups=RepoGroup.get_all_repo_groups(), diff --git a/rhodecode/config/config_maker.py b/rhodecode/config/config_maker.py index f33fd929..0ac075d9 100644 --- a/rhodecode/config/config_maker.py +++ b/rhodecode/config/config_maker.py @@ -117,6 +117,8 @@ def sanitize_settings_and_apply_defaults(global_config, settings): settings_maker.make_setting('vcs.methods.cache', True, parser='bool') + # repo_store path + settings_maker.make_setting('repo_store.path', '/var/opt/rhodecode_repo_store') # Support legacy values of vcs.scm_app_implementation. Legacy # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'. diff --git a/rhodecode/config/environment.py b/rhodecode/config/environment.py index 376cad85..b67da521 100644 --- a/rhodecode/config/environment.py +++ b/rhodecode/config/environment.py @@ -81,7 +81,6 @@ def load_pyramid_environment(global_config, settings): rhodecode.PYRAMID_SETTINGS = settings_merged rhodecode.CONFIG = settings_merged rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id() - rhodecode.CONFIG['default_base_path'] = utils.get_default_base_path() if vcs_server_enabled: connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings)) diff --git a/rhodecode/config/utils.py b/rhodecode/config/utils.py index 6f443954..41f8c9dc 100644 --- a/rhodecode/config/utils.py +++ b/rhodecode/config/utils.py @@ -102,15 +102,3 @@ def get_default_user_id(): user_id = result.first()[0] return user_id - - -def get_default_base_path(): - from sqlalchemy import text - from rhodecode.model import meta - - engine = meta.get_engine() - with meta.SA_Session(engine) as session: - result = session.execute(text("SELECT ui_value from rhodecode_ui where ui_key = '/'")) - base_path = result.first()[0] - - return base_path diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py index da8104fe..c82ef08e 100644 --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -347,8 +347,6 @@ def attach_context_attributes(context, request, user_id=None, is_api=None): context.ssh_key_generator_enabled = str2bool( config.get('ssh.enable_ui_key_generator', 'true')) - context.visual.allow_repo_location_change = str2bool( - config.get('allow_repo_location_change', True)) context.visual.allow_custom_hooks_settings = str2bool( config.get('allow_custom_hooks_settings', True)) context.debug_style = str2bool(config.get('debug_style', False)) diff --git a/rhodecode/lib/middleware/simplevcs.py b/rhodecode/lib/middleware/simplevcs.py index 57415de9..7f7f2a1c 100644 --- a/rhodecode/lib/middleware/simplevcs.py +++ b/rhodecode/lib/middleware/simplevcs.py @@ -156,17 +156,10 @@ class SimpleVCS(object): @property def base_path(self): - settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING) + settings_path = self.config.get('repo_store.path') if not settings_path: - settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING) - - if not settings_path: - # try, maybe we passed in explicitly as config option - settings_path = self.config.get('base_path') - - if not settings_path: - raise ValueError('FATAL: base_path is empty') + raise ValueError('FATAL: repo_store.path is empty') return settings_path def set_repo_names(self, environ): diff --git a/rhodecode/lib/system_info.py b/rhodecode/lib/system_info.py index 43ad948e..a38a1890 100644 --- a/rhodecode/lib/system_info.py +++ b/rhodecode/lib/system_info.py @@ -331,8 +331,8 @@ def cpu(): @register_sysinfo def storage(): from rhodecode.lib.helpers import format_byte_size_binary - from rhodecode.model.settings import VcsSettingsModel - path = VcsSettingsModel().get_repos_location() + from rhodecode.lib.utils import get_rhodecode_repo_store_path + path = get_rhodecode_repo_store_path() value = dict(percent=0, used=0, total=0, path=path, text='') state = STATE_OK_DEFAULT @@ -364,8 +364,8 @@ def storage(): @register_sysinfo def storage_inodes(): - from rhodecode.model.settings import VcsSettingsModel - path = VcsSettingsModel().get_repos_location() + from rhodecode.lib.utils import get_rhodecode_repo_store_path + path = get_rhodecode_repo_store_path() value = dict(percent=0.0, free=0, used=0, total=0, path=path, text='') state = STATE_OK_DEFAULT @@ -442,11 +442,10 @@ def storage_archives(): @register_sysinfo def storage_gist(): from rhodecode.model.gist import GIST_STORE_LOC - from rhodecode.model.settings import VcsSettingsModel - from rhodecode.lib.utils import safe_str + from rhodecode.lib.utils import safe_str, get_rhodecode_repo_store_path from rhodecode.lib.helpers import format_byte_size_binary path = safe_str(os.path.join( - VcsSettingsModel().get_repos_location(), GIST_STORE_LOC)) + get_rhodecode_repo_store_path(), GIST_STORE_LOC)) # gist storage value = dict(percent=0, used=0, total=0, items=0, path=path, text='') diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py index d331d7cd..ac07c77e 100644 --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -472,14 +472,14 @@ def get_rhodecode_realm(): return safe_str(realm.app_settings_value) -def get_rhodecode_base_path(): +def get_rhodecode_repo_store_path(): """ Returns the base path. The base path is the filesystem path which points to the repository store. """ import rhodecode - return rhodecode.CONFIG['default_base_path'] + return rhodecode.CONFIG['repo_store.path'] def map_groups(path): diff --git a/rhodecode/model/__init__.py b/rhodecode/model/__init__.py index 0805a867..2683acc7 100644 --- a/rhodecode/model/__init__.py +++ b/rhodecode/model/__init__.py @@ -21,6 +21,7 @@ import logging import rhodecode from rhodecode.model import meta, db +from rhodecode.lib.utils import get_rhodecode_repo_store_path from rhodecode.lib.utils2 import obfuscate_url_pw, get_encryption_key log = logging.getLogger(__name__) @@ -138,3 +139,11 @@ class BaseModel(object): Returns all instances of what is defined in `cls` class variable """ return cls.cls.getAll() + + @property + def repos_path(self): + """ + Gets the repositories root path from *ini file + """ + + return get_rhodecode_repo_store_path() diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index 34d74e5f..63e095bd 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -1942,8 +1942,8 @@ class Repository(Base, BaseModel): :param cls: """ - from rhodecode.lib.utils import get_rhodecode_base_path - return get_rhodecode_base_path() + from rhodecode.lib.utils import get_rhodecode_repo_store_path + return get_rhodecode_repo_store_path() @classmethod def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None), @@ -2009,16 +2009,13 @@ class Repository(Base, BaseModel): def groups_and_repo(self): return self.groups_with_parents, self - @LazyProperty + @property def repo_path(self): """ Returns base full path for that repository means where it actually exists on a filesystem """ - q = Session().query(RhodeCodeUi).filter( - RhodeCodeUi.ui_key == self.NAME_SEP) - q = q.options(FromCache("sql_cache_short", "repository_repo_path")) - return q.one().ui_value + return self.base_path() @property def repo_full_path(self): diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index 39d3b21c..d05871ae 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -434,10 +434,6 @@ def ApplicationUiSettingsForm(localizer): class _ApplicationUiSettingsForm(_BaseVcsSettingsForm): web_push_ssl = v.StringBoolean(if_missing=False) - paths_root_path = All( - v.ValidPath(localizer), - v.UnicodeString(strip=True, min=1, not_empty=True) - ) largefiles_usercache = All( v.ValidPath(localizer), v.UnicodeString(strip=True, min=2, not_empty=True)) diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index e784833c..12f37b9a 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -83,14 +83,6 @@ class RepoModel(BaseModel): return repo_to_perm - @LazyProperty - def repos_path(self): - """ - Gets the repositories root path from database - """ - settings_model = VcsSettingsModel(sa=self.sa) - return settings_model.get_repos_location() - def get(self, repo_id): repo = self.sa.query(Repository) \ .filter(Repository.repo_id == repo_id) diff --git a/rhodecode/model/repo_group.py b/rhodecode/model/repo_group.py index 2d754169..64f6db56 100644 --- a/rhodecode/model/repo_group.py +++ b/rhodecode/model/repo_group.py @@ -62,15 +62,6 @@ class RepoGroupModel(BaseModel): def get_repo_group(self, repo_group): return self._get_repo_group(repo_group) - @LazyProperty - def repos_path(self): - """ - Gets the repositories root path from database - """ - - settings_model = VcsSettingsModel(sa=self.sa) - return settings_model.get_repos_location() - def get_by_group_name(self, repo_group_name, cache=None): repo = self.sa.query(RepoGroup) \ .filter(RepoGroup.group_name == repo_group_name) diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py index 560abe59..30228e6b 100644 --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -189,15 +189,6 @@ class ScmModel(BaseModel): Generic Scm Model """ - @LazyProperty - def repos_path(self): - """ - Gets the repositories root path from database - """ - - settings_model = VcsSettingsModel(sa=self.sa) - return settings_model.get_repos_location() - def repo_scan(self, repos_path=None): """ Listing of repositories in given path. This path should not be a diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py index 0beff5fe..3b3f15dc 100644 --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -811,9 +811,6 @@ class VcsSettingsModel(object): else: return self.get_repo_general_settings() - def get_repos_location(self): - return self.global_settings.get_ui_by_key('/').ui_value - def _filter_ui_settings(self, settings): filtered_settings = [ s for s in settings if self._should_keep_setting(s)] diff --git a/rhodecode/subscribers.py b/rhodecode/subscribers.py index b217c3e5..07c04fd9 100644 --- a/rhodecode/subscribers.py +++ b/rhodecode/subscribers.py @@ -116,8 +116,9 @@ def scan_repositories_if_enabled(event): import_on_startup = settings['startup.import_repos'] if vcs_server_enabled and import_on_startup: from rhodecode.model.scm import ScmModel - from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path - repositories = ScmModel().repo_scan(get_rhodecode_base_path()) + from rhodecode.lib.utils import repo2db_mapper + scm = ScmModel() + repositories = scm.repo_scan(scm.repos_path) repo2db_mapper(repositories, remove_obsolete=False) diff --git a/rhodecode/templates/admin/settings/settings_vcs.mako b/rhodecode/templates/admin/settings/settings_vcs.mako index 4f37f110..4b6dd11a 100644 --- a/rhodecode/templates/admin/settings/settings_vcs.mako +++ b/rhodecode/templates/admin/settings/settings_vcs.mako @@ -6,8 +6,7 @@ ${h.secure_form(h.route_path('admin_settings_vcs_update'), request=request)} suffix='', svn_tag_patterns=c.svn_tag_patterns, svn_branch_patterns=c.svn_branch_patterns, - display_globals=True, - allow_repo_location_change=c.visual.allow_repo_location_change + display_globals=True )}
${h.submit('save',_('Save settings'),class_="btn")} diff --git a/rhodecode/templates/base/vcs_settings.mako b/rhodecode/templates/base/vcs_settings.mako index d9b80493..1b2feb17 100644 --- a/rhodecode/templates/base/vcs_settings.mako +++ b/rhodecode/templates/base/vcs_settings.mako @@ -3,7 +3,7 @@ ## <%namespace name="vcss" file="/base/vcssettings.mako"/> ## ${vcss.vcs_settings_fields()} -<%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, allow_repo_location_change=False, **kwargs)"> +<%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, **kwargs)"> % if display_globals:
@@ -23,34 +23,6 @@
% endif - % if display_globals: -
-
-

${_('Main Storage Location')}

-
-
-
-
- %if allow_repo_location_change: - ${h.text('paths_root_path',size=59,readonly="readonly", class_="disabled")} - -
-
- %else: - ${_('Repository location change is disabled. You can enable this by changing the `allow_repo_location_change` inside .ini file.')} - ## form still requires this but we cannot internally change it anyway - ${h.hidden('paths_root_path',size=30,readonly="readonly", class_="disabled")} - %endif -
-
-
- ${_('Filesystem location where repositories should be stored. After changing this value a restart and rescan of the repository folder are required.')} -
-
-
- % endif - % if display_globals or repo_type in ['git', 'hg']:
diff --git a/rhodecode/tests/__init__.py b/rhodecode/tests/__init__.py index e507f3c2..55591696 100644 --- a/rhodecode/tests/__init__.py +++ b/rhodecode/tests/__init__.py @@ -27,6 +27,7 @@ import urllib.parse import pytest +import rhodecode from rhodecode.model.db import User from rhodecode.lib import auth from rhodecode.lib import helpers as h @@ -53,8 +54,11 @@ __all__ = [ # SOME GLOBALS FOR TESTS TEST_DIR = tempfile.gettempdir() +if os.getenv("RC_TEST"): + rhodecode.CONFIG['repo_store.path'] = TEST_DIR -TESTS_TMP_PATH = jn(TEST_DIR, 'rc_test_{}'.format(next(tempfile._RandomNameSequence()))) +# Once repo_store.path in test config will be changed update TEST_TMP_PATH accordingly +TESTS_TMP_PATH = jn(TEST_DIR, 'rc_test_a0hmmrva') TEST_USER_ADMIN_LOGIN = 'test_admin' TEST_USER_ADMIN_PASS = 'test12' TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com' @@ -111,7 +115,7 @@ def get_new_dir(title): hex_str = sha1_safe(f'{os.getpid()} {time.time()}') name_parts.append(hex_str) name = '-'.join(name_parts) - path = os.path.join(TEST_DIR, name) + path = jn(TEST_DIR, name) return get_normalized_path(path) diff --git a/rhodecode/tests/lib/middleware/test_simplesvn.py b/rhodecode/tests/lib/middleware/test_simplesvn.py index 21a5a890..5e73634e 100644 --- a/rhodecode/tests/lib/middleware/test_simplesvn.py +++ b/rhodecode/tests/lib/middleware/test_simplesvn.py @@ -23,16 +23,16 @@ import pytest from unittest.mock import patch, Mock, MagicMock from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp -from rhodecode.lib.utils import get_rhodecode_base_path +from rhodecode.lib.utils import get_rhodecode_repo_store_path from rhodecode.tests import SVN_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS class TestSimpleSvn(object): @pytest.fixture(autouse=True) def simple_svn(self, baseapp, request_stub): - base_path = get_rhodecode_base_path() + base_path = get_rhodecode_repo_store_path() self.app = SimpleSvn( - config={'auth_ret_code': '', 'base_path': base_path}, + config={'auth_ret_code': '', 'repo_store.path': base_path}, registry=request_stub.registry) def test_get_config(self): @@ -126,7 +126,7 @@ class TestSimpleSvnApp(object): def setup_method(self, method): # note(marcink): this is hostname from docker compose used for testing... self.host = 'http://svn:8090' - base_path = get_rhodecode_base_path() + base_path = get_rhodecode_repo_store_path() self.app = SimpleSvnApp( config={'subversion_http_server_url': self.host, 'base_path': base_path}) diff --git a/rhodecode/tests/models/settings/test_vcs_settings.py b/rhodecode/tests/models/settings/test_vcs_settings.py index fe3dcc23..946ce127 100644 --- a/rhodecode/tests/models/settings/test_vcs_settings.py +++ b/rhodecode/tests/models/settings/test_vcs_settings.py @@ -1004,21 +1004,6 @@ class TestGetSvnPatterns(object): settings_mock.assert_called_once_with(*args) -class TestGetReposLocation(object): - def test_returns_repos_location(self, repo_stub): - model = VcsSettingsModel() - - result_mock = mock.Mock() - result_mock.ui_value = '/tmp' - - with mock.patch.object(model, 'global_settings') as settings_mock: - settings_mock.get_ui_by_key.return_value = result_mock - result = model.get_repos_location() - - settings_mock.get_ui_by_key.assert_called_once_with('/') - assert result == '/tmp' - - class TestCreateOrUpdateRepoSettings(object): FORM_DATA = { 'inherit_global_settings': False, diff --git a/rhodecode/tests/models/test_repos.py b/rhodecode/tests/models/test_repos.py index cc0a768f..0ba88a4a 100644 --- a/rhodecode/tests/models/test_repos.py +++ b/rhodecode/tests/models/test_repos.py @@ -121,20 +121,20 @@ class TestRepoModel(object): def test_create_filesystem_repo_installs_hooks(self, tmpdir, backend): repo = backend.create_repo() repo_name = repo.repo_name - model = RepoModel() - repo_location = tempfile.mkdtemp() - model.repos_path = repo_location - repo = model._create_filesystem_repo( - repo_name, backend.alias, repo_group='', clone_uri=None) + with mock.patch('rhodecode.model.repo.RepoModel.repos_path', + new_callable=mock.PropertyMock) as mocked_models_property: + mocked_models_property.return_value = tempfile.mkdtemp() + repo = RepoModel()._create_filesystem_repo( + repo_name, backend.alias, repo_group='', clone_uri=None) - hooks = { - 'svn': ('pre-commit', 'post-commit'), - 'git': ('pre-receive', 'post-receive'), - } - for hook in hooks[backend.alias]: - with open(os.path.join(repo.path, 'hooks', hook)) as f: - data = f.read() - assert 'RC_HOOK_VER' in data + hooks = { + 'svn': ('pre-commit', 'post-commit'), + 'git': ('pre-receive', 'post-receive'), + } + for hook in hooks[backend.alias]: + with open(os.path.join(repo.path, 'hooks', hook)) as f: + data = f.read() + assert 'RC_HOOK_VER' in data @pytest.mark.parametrize("use_global_config, repo_name_passed", [ (True, False), diff --git a/rhodecode/tests/rhodecode.ini b/rhodecode/tests/rhodecode.ini index f01777ef..72fb2c9e 100644 --- a/rhodecode/tests/rhodecode.ini +++ b/rhodecode/tests/rhodecode.ini @@ -207,9 +207,6 @@ auth_ret_code_detection = false ; codes don't break the transactions while 4XX codes do lock_ret_code = 423 -; allows to change the repository location in settings page -allow_repo_location_change = true - ; allows to setup custom hooks in settings page allow_custom_hooks_settings = true From 216e86d96eee3f3c41af266534972fa2005e2ece Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Fri, 1 Mar 2024 11:43:04 +0100 Subject: [PATCH 025/150] dev(makefile): hide prompt info on dev sh env --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3d59ea61..e7112cec 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ dev-sh: curl https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh | sh @echo "source <(carapace _carapace)" > /home/rhodecode/.zsrc @echo "${RC_DEV_CMD_HELP}" - PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# ' zsh + @PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# ' zsh .PHONY: dev-cleanup From 747534f76d14b0c1646203dca2c340191cc1358c Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Fri, 1 Mar 2024 13:40:11 +0100 Subject: [PATCH 026/150] fix(svn-ssh): fixed svn ssh wrapper --- .../apps/ssh_support/lib/backends/svn.py | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/rhodecode/apps/ssh_support/lib/backends/svn.py b/rhodecode/apps/ssh_support/lib/backends/svn.py index d9f1f858..7b728d75 100644 --- a/rhodecode/apps/ssh_support/lib/backends/svn.py +++ b/rhodecode/apps/ssh_support/lib/backends/svn.py @@ -25,6 +25,8 @@ import tempfile from subprocess import Popen, PIPE import urllib.parse +from rhodecode.lib.str_utils import safe_bytes +from rhodecode_tools.lib.utils import safe_str from .base import SshVcsServer log = logging.getLogger(__name__) @@ -81,7 +83,7 @@ class SubversionTunnelWrapper(object): def sync(self): while self.process.poll() is None: - next_byte = self.stdin.read(1) + next_byte = self.stdin.buffer.read(1) if not next_byte: break self.process.stdin.write(next_byte) @@ -106,7 +108,7 @@ class SubversionTunnelWrapper(object): data['url'] = self._svn_string(data['url']) data['ra_client'] = self._svn_string(data['ra_client']) data['client'] = data['client'] or '' - buffer_ = ( + buffer_ = safe_bytes( "( {version} ( {capabilities} ) {url}{ra_client}" "( {client}) ) ".format(**data)) self.process.stdin.write(buffer_) @@ -127,21 +129,21 @@ class SubversionTunnelWrapper(object): return f'{len(str_)}:{str_} ' def _read_first_client_response(self): - buffer_ = "" + buffer_ = b"" brackets_stack = [] while True: - next_byte = self.stdin.read(1) + next_byte = self.stdin.buffer.read(1) buffer_ += next_byte - if next_byte == "(": + if next_byte == b"(": brackets_stack.append(next_byte) - elif next_byte == ")": + elif next_byte == b")": brackets_stack.pop() - elif next_byte == " " and not brackets_stack: + elif next_byte == b" " and not brackets_stack: break return buffer_ - def _parse_first_client_response(self, buffer_): + def _parse_first_client_response(self, buffer_: bytes): """ According to the Subversion RA protocol, the first request should look like: @@ -151,16 +153,20 @@ class SubversionTunnelWrapper(object): Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol """ - version_re = r'(?P\d+)' - capabilities_re = r'\(\s(?P[\w\d\-\ ]+)\s\)' - url_re = r'\d+\:(?P[\W\w]+)' - ra_client_re = r'(\d+\:(?P[\W\w]+)\s)' - client_re = r'(\d+\:(?P[\W\w]+)\s)*' + version_re = br'(?P\d+)' + capabilities_re = br'\(\s(?P[\w\d\-\ ]+)\s\)' + url_re = br'\d+\:(?P[\W\w]+)' + ra_client_re = br'(\d+\:(?P[\W\w]+)\s)' + client_re = br'(\d+\:(?P[\W\w]+)\s)*' regex = re.compile( - r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}' - r'\(\s{client}\)\s\)\s*$'.format( - version=version_re, capabilities=capabilities_re, - url=url_re, ra_client=ra_client_re, client=client_re)) + br'^\(\s%b\s%b\s%b\s%b' + br'\(\s%b\)\s\)\s*$' % ( + version_re, + capabilities_re, + url_re, + ra_client_re, + client_re) + ) matcher = regex.match(buffer_) return matcher.groupdict() if matcher else None @@ -202,7 +208,7 @@ class SubversionTunnelWrapper(object): url_parts = urllib.parse.urlparse(first_response['url']) - self.server.repo_name = self._match_repo_name(url_parts.path.strip('/')) + self.server.repo_name = self._match_repo_name(safe_str(url_parts.path).strip('/')) exit_code = self.server._check_permissions(action) if exit_code: From 0a814bd6b8f3a80c34313fd642604cf642234149 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Fri, 1 Mar 2024 13:49:28 +0100 Subject: [PATCH 027/150] fix(svn): ensure we have defaults for old .ini files and upgrades --- rhodecode/config/config_maker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rhodecode/config/config_maker.py b/rhodecode/config/config_maker.py index f33fd929..d27897fe 100644 --- a/rhodecode/config/config_maker.py +++ b/rhodecode/config/config_maker.py @@ -94,6 +94,7 @@ def sanitize_settings_and_apply_defaults(global_config, settings): settings_maker.make_setting('default_encoding', 'UTF-8', parser='list') settings_maker.make_setting('is_test', False, parser='bool') settings_maker.make_setting('gzip_responses', False, parser='bool') + settings_maker.make_setting('startup.import_repos', 'false', parser='bool') # statsd settings_maker.make_setting('statsd.enabled', False, parser='bool') @@ -103,13 +104,14 @@ def sanitize_settings_and_apply_defaults(global_config, settings): settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool') settings_maker.make_setting('vcs.svn.compatible_version', '') + settings_maker.make_setting('vcs.svn.proxy.enabled', True, parser='bool') + settings_maker.make_setting('vcs.svn.proxy.host', 'http://svn:8090', parser='string') settings_maker.make_setting('vcs.hooks.protocol', 'http') settings_maker.make_setting('vcs.hooks.host', '*') settings_maker.make_setting('vcs.scm_app_implementation', 'http') settings_maker.make_setting('vcs.server', '') settings_maker.make_setting('vcs.server.protocol', 'http') settings_maker.make_setting('vcs.server.enable', 'true', parser='bool') - settings_maker.make_setting('startup.import_repos', 'false', parser='bool') settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool') settings_maker.make_setting('vcs.start_server', 'false', parser='bool') settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list') From b79db3eb9cb2284bef9214a040bd95e24a72990c Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Fri, 1 Mar 2024 16:19:48 +0100 Subject: [PATCH 028/150] fix(tests): fixed bad value for default binary_dir value --- rhodecode/tests/vcsserver_http.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rhodecode/tests/vcsserver_http.ini b/rhodecode/tests/vcsserver_http.ini index 36dd49f7..461cf14c 100644 --- a/rhodecode/tests/vcsserver_http.ini +++ b/rhodecode/tests/vcsserver_http.ini @@ -33,7 +33,7 @@ locale = en_US.UTF-8 ; path to binaries for vcsserver, it should be set by the installer ; at installation time, e.g /home/user/vcsserver-1/profile/bin ; it can also be a path to nix-build output in case of development -core.binary_dir = "" +core.binary_dir = ; Custom exception store path, defaults to TMPDIR ; This is used to store exception from RhodeCode in shared directory From 104352ccda7dd3ed1eae18ca649d611278f4e807 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Sun, 3 Mar 2024 08:40:12 +0100 Subject: [PATCH 029/150] fix(svn): more binary protocol svn ssh fixes --- .../apps/ssh_support/lib/backends/svn.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/rhodecode/apps/ssh_support/lib/backends/svn.py b/rhodecode/apps/ssh_support/lib/backends/svn.py index 7b728d75..b9273fe8 100644 --- a/rhodecode/apps/ssh_support/lib/backends/svn.py +++ b/rhodecode/apps/ssh_support/lib/backends/svn.py @@ -25,7 +25,6 @@ import tempfile from subprocess import Popen, PIPE import urllib.parse -from rhodecode.lib.str_utils import safe_bytes from rhodecode_tools.lib.utils import safe_str from .base import SshVcsServer @@ -103,19 +102,27 @@ class SubversionTunnelWrapper(object): def patch_first_client_response(self, response, **kwargs): self.create_hooks_env() - data = response.copy() - data.update(kwargs) - data['url'] = self._svn_string(data['url']) - data['ra_client'] = self._svn_string(data['ra_client']) - data['client'] = data['client'] or '' - buffer_ = safe_bytes( - "( {version} ( {capabilities} ) {url}{ra_client}" - "( {client}) ) ".format(**data)) + + version = response['version'] + capabilities = response['capabilities'] + client = response['client'] or b'' + + url = self._svn_bytes(response['url']) + ra_client = self._svn_bytes(response['ra_client']) + + buffer_ = b"( %b ( %b ) %b%b( %b) ) " % ( + version, + capabilities, + url, + ra_client, + client + ) self.process.stdin.write(buffer_) def fail(self, message): - print("( failure ( ( 210005 {message} 0: 0 ) ) )".format( - message=self._svn_string(message))) + fail_msg = b"( failure ( ( 210005 %b 0: 0 ) ) )" % self._svn_bytes(message) + sys.stdout.buffer.write(fail_msg) + sys.stdout.flush() self.remove_configs() self.process.kill() return 1 @@ -123,10 +130,11 @@ class SubversionTunnelWrapper(object): def interrupt(self, signum, frame): self.fail("Exited by timeout") - def _svn_string(self, str_): - if not str_: - return '' - return f'{len(str_)}:{str_} ' + def _svn_bytes(self, bytes_: bytes) -> bytes: + if not bytes_: + return b'' + + return f'{len(bytes_)}:'.encode() + bytes_ + b' ' def _read_first_client_response(self): buffer_ = b"" From f928ad9b22e546f83bdfd4c11443b28cd64010ed Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Sun, 3 Mar 2024 08:40:40 +0100 Subject: [PATCH 030/150] feat(mercurial): added close branch commit message template --- rhodecode/lib/vcs/backends/hg/repository.py | 8 +++++++- rhodecode/lib/vcs/conf/settings.py | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/rhodecode/lib/vcs/backends/hg/repository.py b/rhodecode/lib/vcs/backends/hg/repository.py index 9d213316..8b2330e1 100644 --- a/rhodecode/lib/vcs/backends/hg/repository.py +++ b/rhodecode/lib/vcs/backends/hg/repository.py @@ -35,6 +35,7 @@ from rhodecode.lib.datelib import ( from rhodecode.lib.str_utils import safe_str from rhodecode.lib.utils2 import CachedProperty from rhodecode.lib.vcs import connection, exceptions +from rhodecode.lib.vcs.conf import settings as vcs_settings from rhodecode.lib.vcs.backends.base import ( BaseRepository, CollectionGenerator, Config, MergeResponse, MergeFailureReason, Reference, BasePathPermissionChecker) @@ -722,7 +723,12 @@ class MercurialRepository(BaseRepository): commit needs to be pushed. """ self._update(source_ref.commit_id) - message = close_message or f"Closing branch: `{source_ref.name}`" + message = (close_message or vcs_settings.HG_CLOSE_BRANCH_MESSAGE_TMPL).format( + user_name=user_name, + user_email=user_email, + target_ref_name=target_ref.name, + source_ref_name=source_ref.name + ) try: self._remote.commit( message=safe_str(message), diff --git a/rhodecode/lib/vcs/conf/settings.py b/rhodecode/lib/vcs/conf/settings.py index 93dacded..8a8640a5 100644 --- a/rhodecode/lib/vcs/conf/settings.py +++ b/rhodecode/lib/vcs/conf/settings.py @@ -58,6 +58,9 @@ MERGE_MESSAGE_TMPL = ( MERGE_DRY_RUN_MESSAGE = 'dry_run_merge_message_from_rhodecode' MERGE_DRY_RUN_USER = 'Dry-Run User' MERGE_DRY_RUN_EMAIL = 'dry-run-merge@rhodecode.com' +HG_CLOSE_BRANCH_MESSAGE_TMPL = ( + 'Closing branch: `{source_ref_name}`' +) def available_aliases(): From 2dd2eea658ee6e3f06a1dc7177871911f52aead9 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 4 Mar 2024 11:34:15 +0100 Subject: [PATCH 031/150] chore(configs): optimize configs for docker env --- configs/development.ini | 123 ++++++++++----------------------------- configs/production.ini | 125 +++++++++++----------------------------- 2 files changed, 65 insertions(+), 183 deletions(-) diff --git a/configs/development.ini b/configs/development.ini index 7b63786e..0e5aea4f 100644 --- a/configs/development.ini +++ b/configs/development.ini @@ -305,7 +305,7 @@ file_store.enabled = true file_store.backend = local ; path to store the uploaded binaries -file_store.storage_path = %(here)s/data/file_store +file_store.storage_path = /var/opt/rhodecode_data/file_store ; Uncomment and set this path to control settings for archive download cache. ; Generated repo archives will be cached at this location @@ -314,7 +314,7 @@ file_store.storage_path = %(here)s/data/file_store ; RhodeCode and vcsserver ; Default is $cache_dir/archive_cache if not set -archive_cache.store_dir = %(here)s/data/archive_cache +archive_cache.store_dir = /var/opt/rhodecode_data/tarballcache ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb archive_cache.cache_size_gb = 10 @@ -328,7 +328,7 @@ archive_cache.cache_shards = 10 ; manually run celery: /path/to/celery worker --task-events --beat --app rhodecode.lib.celerylib.loader --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler --loglevel DEBUG --ini /path/to/rhodecode.ini -use_celery = false +use_celery = true ; path to store schedule database #celerybeat-schedule.path = @@ -354,7 +354,7 @@ celery.task_always_eager = false ; Default cache dir for caches. Putting this into a ramdisk can boost performance. ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space -cache_dir = %(here)s/data +cache_dir = /var/opt/rhodecode_data ; ********************************************* ; `sql_cache_short` cache for heavy SQL queries @@ -463,12 +463,12 @@ rc_cache.cache_repo.expiration_time = 2592000 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed ; types are file, ext:redis, ext:database, ext:memcached ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session -beaker.session.type = file -beaker.session.data_dir = %(here)s/data/sessions +#beaker.session.type = file +#beaker.session.data_dir = %(here)s/data/sessions ; Redis based sessions -#beaker.session.type = ext:redis -#beaker.session.url = redis://127.0.0.1:6379/2 +beaker.session.type = ext:redis +beaker.session.url = redis://redis:6379/2 ; DB based session, fast, and allows easy management over logged in users #beaker.session.type = ext:database @@ -480,7 +480,7 @@ beaker.session.data_dir = %(here)s/data/sessions beaker.session.key = rhodecode beaker.session.secret = develop-rc-uytcxaz -beaker.session.lock_dir = %(here)s/data/sessions/lock +beaker.session.lock_dir = /data_ramdisk/lock ; Secure encrypted cookie. Requires AES and AES python libraries ; you must disable beaker.session.secret to use this @@ -521,18 +521,18 @@ search.location = %(here)s/data/index ; channelstream enables persistent connections and live notification ; in the system. It's also used by the chat system -channelstream.enabled = false +channelstream.enabled = true ; server address for channelstream server on the backend -channelstream.server = 127.0.0.1:9800 +channelstream.server = channelstream:9800 ; location of the channelstream server from outside world ; use ws:// for http or wss:// for https. This address needs to be handled ; by external HTTP server such as Nginx or Apache ; see Nginx/Apache configuration examples in our docs channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream -channelstream.secret = secret -channelstream.history.location = %(here)s/channelstream_history +channelstream.secret = ENV_GENERATED +channelstream.history.location = /var/opt/rhodecode_data/channelstream_history ; Internal application path that Javascript uses to connect into. ; If you use proxy-prefix the prefix should be added before /_channelstream @@ -578,7 +578,7 @@ sqlalchemy.db1.pool_recycle = 3600 ; VCS CONFIG ; ########## vcs.server.enable = true -vcs.server = localhost:9900 +vcs.server = vcsserver:10010 ; Web server connectivity protocol, responsible for web based VCS operations ; Available protocols are: @@ -610,6 +610,15 @@ vcs.backends = hg, git, svn ; Wait this number of seconds before killing connection to the vcsserver vcs.connection_timeout = 3600 +; Cache flag to cache vcsserver remote calls locally +; It uses cache_region `cache_repo` +vcs.methods.cache = true + +; #################################################### +; Subversion proxy support (mod_dav_svn) +; Maps RhodeCode repo groups into SVN paths for Apache +; #################################################### + ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out. ; Set a numeric version for your current SVN e.g 1.8, or 1.12 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible @@ -621,23 +630,14 @@ vcs.svn.proxy.enabled = true ; host to connect to running SVN subsystem vcs.svn.proxy.host = http://svn:8090 -; Cache flag to cache vcsserver remote calls locally -; It uses cache_region `cache_repo` -vcs.methods.cache = true - -; #################################################### -; Subversion proxy support (mod_dav_svn) -; Maps RhodeCode repo groups into SVN paths for Apache -; #################################################### - ; Enable or disable the config file generation. -svn.proxy.generate_config = false +svn.proxy.generate_config = true ; Generate config file with `SVNListParentPath` set to `On`. svn.proxy.list_parent_path = true ; Set location and file name of generated config file. -svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf +svn.proxy.config_file_path = /etc/rhodecode/conf/svn/mod_dav_svn.conf ; alternative mod_dav config template. This needs to be a valid mako template ; Example template can be found in the source code: @@ -665,7 +665,7 @@ svn.proxy.location_root = / ; any change user ssh keys. Setting this to false also disables possibility ; of adding SSH keys by users from web interface. Super admins can still ; manage SSH Keys. -ssh.generate_authorized_keyfile = false +ssh.generate_authorized_keyfile = true ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding` # ssh.authorized_keys_ssh_opts = @@ -673,12 +673,12 @@ ssh.generate_authorized_keyfile = false ; Path to the authorized_keys file where the generate entries are placed. ; It is possible to have multiple key files specified in `sshd_config` e.g. ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode -ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode +ssh.authorized_keys_file_path = /etc/rhodecode/conf/ssh/authorized_keys_rhodecode ; Command to execute the SSH wrapper. The binary is available in the ; RhodeCode installation directory. -; e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper -ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper +; e.g /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper +ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper ; Allow shell when executing the ssh-wrapper command ssh.wrapper_cmd_allow_shell = false @@ -689,73 +689,14 @@ ssh.enable_debug_logging = true ; Paths to binary executable, by default they are the names, but we can ; override them if we want to use a custom one -ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg -ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git -ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve +ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg +ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git +ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve ; Enables SSH key generator web interface. Disabling this still allows users ; to add their own keys. ssh.enable_ui_key_generator = true - -; ################# -; APPENLIGHT CONFIG -; ################# - -; Appenlight is tailored to work with RhodeCode, see -; http://appenlight.rhodecode.com for details how to obtain an account - -; Appenlight integration enabled -#appenlight = false - -#appenlight.server_url = https://api.appenlight.com -#appenlight.api_key = YOUR_API_KEY -#appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5 - -; used for JS client -#appenlight.api_public_key = YOUR_API_PUBLIC_KEY - -; TWEAK AMOUNT OF INFO SENT HERE - -; enables 404 error logging (default False) -#appenlight.report_404 = false - -; time in seconds after request is considered being slow (default 1) -#appenlight.slow_request_time = 1 - -; record slow requests in application -; (needs to be enabled for slow datastore recording and time tracking) -#appenlight.slow_requests = true - -; enable hooking to application loggers -#appenlight.logging = true - -; minimum log level for log capture -#ppenlight.logging.level = WARNING - -; send logs only from erroneous/slow requests -; (saves API quota for intensive logging) -#appenlight.logging_on_error = false - -; list of additional keywords that should be grabbed from environ object -; can be string with comma separated list of words in lowercase -; (by default client will always send following info: -; 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that -; start with HTTP* this list be extended with additional keywords here -#appenlight.environ_keys_whitelist = - -; list of keywords that should be blanked from request object -; can be string with comma separated list of words in lowercase -; (by default client will always blank keys that contain following words -; 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf' -; this list be extended with additional keywords set here -#appenlight.request_keys_blacklist = - -; list of namespaces that should be ignores when gathering log entries -; can be string with comma separated list of namespaces -; (by default the client ignores own entries: appenlight_client.client) -#appenlight.log_namespace_blacklist = - ; Statsd client config, this is used to send metrics to statsd ; We recommend setting statsd_exported and scrape them using Prometheus #statsd.enabled = false diff --git a/configs/production.ini b/configs/production.ini index 75a8de1f..fe05826b 100644 --- a/configs/production.ini +++ b/configs/production.ini @@ -105,7 +105,7 @@ startup.import_repos = false app.base_url = http://rhodecode.local ; Host at which the Service API is running. -app.service_api.host= http://rhodecode.local:10020 +app.service_api.host = http://rhodecode.local:10020 ; Secret for Service API authentication. app.service_api.token = @@ -256,7 +256,7 @@ file_store.enabled = true file_store.backend = local ; path to store the uploaded binaries -file_store.storage_path = %(here)s/data/file_store +file_store.storage_path = /var/opt/rhodecode_data/file_store ; Uncomment and set this path to control settings for archive download cache. ; Generated repo archives will be cached at this location @@ -265,7 +265,7 @@ file_store.storage_path = %(here)s/data/file_store ; RhodeCode and vcsserver ; Default is $cache_dir/archive_cache if not set -archive_cache.store_dir = %(here)s/data/archive_cache +archive_cache.store_dir = /var/opt/rhodecode_data/tarballcache ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb archive_cache.cache_size_gb = 40 @@ -279,7 +279,7 @@ archive_cache.cache_shards = 4 ; manually run celery: /path/to/celery worker --task-events --beat --app rhodecode.lib.celerylib.loader --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler --loglevel DEBUG --ini /path/to/rhodecode.ini -use_celery = false +use_celery = true ; path to store schedule database #celerybeat-schedule.path = @@ -305,7 +305,7 @@ celery.task_always_eager = false ; Default cache dir for caches. Putting this into a ramdisk can boost performance. ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space -cache_dir = %(here)s/data +cache_dir = /var/opt/rhodecode_data ; ********************************************* ; `sql_cache_short` cache for heavy SQL queries @@ -414,12 +414,12 @@ rc_cache.cache_repo.expiration_time = 2592000 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed ; types are file, ext:redis, ext:database, ext:memcached ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session -beaker.session.type = file -beaker.session.data_dir = %(here)s/data/sessions +#beaker.session.type = file +#beaker.session.data_dir = %(here)s/data/sessions ; Redis based sessions -#beaker.session.type = ext:redis -#beaker.session.url = redis://127.0.0.1:6379/2 +beaker.session.type = ext:redis +beaker.session.url = redis://redis:6379/2 ; DB based session, fast, and allows easy management over logged in users #beaker.session.type = ext:database @@ -431,7 +431,7 @@ beaker.session.data_dir = %(here)s/data/sessions beaker.session.key = rhodecode beaker.session.secret = production-rc-uytcxaz -beaker.session.lock_dir = %(here)s/data/sessions/lock +beaker.session.lock_dir = /data_ramdisk/lock ; Secure encrypted cookie. Requires AES and AES python libraries ; you must disable beaker.session.secret to use this @@ -472,18 +472,18 @@ search.location = %(here)s/data/index ; channelstream enables persistent connections and live notification ; in the system. It's also used by the chat system -channelstream.enabled = false +channelstream.enabled = true ; server address for channelstream server on the backend -channelstream.server = 127.0.0.1:9800 +channelstream.server = channelstream:9800 ; location of the channelstream server from outside world ; use ws:// for http or wss:// for https. This address needs to be handled ; by external HTTP server such as Nginx or Apache ; see Nginx/Apache configuration examples in our docs channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream -channelstream.secret = secret -channelstream.history.location = %(here)s/channelstream_history +channelstream.secret = ENV_GENERATED +channelstream.history.location = /var/opt/rhodecode_data/channelstream_history ; Internal application path that Javascript uses to connect into. ; If you use proxy-prefix the prefix should be added before /_channelstream @@ -529,7 +529,7 @@ sqlalchemy.db1.pool_recycle = 3600 ; VCS CONFIG ; ########## vcs.server.enable = true -vcs.server = localhost:9900 +vcs.server = vcsserver:10010 ; Web server connectivity protocol, responsible for web based VCS operations ; Available protocols are: @@ -561,6 +561,15 @@ vcs.backends = hg, git, svn ; Wait this number of seconds before killing connection to the vcsserver vcs.connection_timeout = 3600 +; Cache flag to cache vcsserver remote calls locally +; It uses cache_region `cache_repo` +vcs.methods.cache = true + +; #################################################### +; Subversion proxy support (mod_dav_svn) +; Maps RhodeCode repo groups into SVN paths for Apache +; #################################################### + ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out. ; Set a numeric version for your current SVN e.g 1.8, or 1.12 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible @@ -572,23 +581,14 @@ vcs.svn.proxy.enabled = true ; host to connect to running SVN subsystem vcs.svn.proxy.host = http://svn:8090 -; Cache flag to cache vcsserver remote calls locally -; It uses cache_region `cache_repo` -vcs.methods.cache = true - -; #################################################### -; Subversion proxy support (mod_dav_svn) -; Maps RhodeCode repo groups into SVN paths for Apache -; #################################################### - ; Enable or disable the config file generation. -svn.proxy.generate_config = false +svn.proxy.generate_config = true ; Generate config file with `SVNListParentPath` set to `On`. svn.proxy.list_parent_path = true ; Set location and file name of generated config file. -svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf +svn.proxy.config_file_path = /etc/rhodecode/conf/svn/mod_dav_svn.conf ; alternative mod_dav config template. This needs to be a valid mako template ; Example template can be found in the source code: @@ -616,7 +616,7 @@ svn.proxy.location_root = / ; any change user ssh keys. Setting this to false also disables possibility ; of adding SSH keys by users from web interface. Super admins can still ; manage SSH Keys. -ssh.generate_authorized_keyfile = false +ssh.generate_authorized_keyfile = true ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding` # ssh.authorized_keys_ssh_opts = @@ -624,12 +624,12 @@ ssh.generate_authorized_keyfile = false ; Path to the authorized_keys file where the generate entries are placed. ; It is possible to have multiple key files specified in `sshd_config` e.g. ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode -ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode +ssh.authorized_keys_file_path = /etc/rhodecode/conf/ssh/authorized_keys_rhodecode ; Command to execute the SSH wrapper. The binary is available in the ; RhodeCode installation directory. -; e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper -ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper +; e.g /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper +ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper ; Allow shell when executing the ssh-wrapper command ssh.wrapper_cmd_allow_shell = false @@ -640,73 +640,14 @@ ssh.enable_debug_logging = false ; Paths to binary executable, by default they are the names, but we can ; override them if we want to use a custom one -ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg -ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git -ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve +ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg +ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git +ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve ; Enables SSH key generator web interface. Disabling this still allows users ; to add their own keys. ssh.enable_ui_key_generator = true - -; ################# -; APPENLIGHT CONFIG -; ################# - -; Appenlight is tailored to work with RhodeCode, see -; http://appenlight.rhodecode.com for details how to obtain an account - -; Appenlight integration enabled -#appenlight = false - -#appenlight.server_url = https://api.appenlight.com -#appenlight.api_key = YOUR_API_KEY -#appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5 - -; used for JS client -#appenlight.api_public_key = YOUR_API_PUBLIC_KEY - -; TWEAK AMOUNT OF INFO SENT HERE - -; enables 404 error logging (default False) -#appenlight.report_404 = false - -; time in seconds after request is considered being slow (default 1) -#appenlight.slow_request_time = 1 - -; record slow requests in application -; (needs to be enabled for slow datastore recording and time tracking) -#appenlight.slow_requests = true - -; enable hooking to application loggers -#appenlight.logging = true - -; minimum log level for log capture -#ppenlight.logging.level = WARNING - -; send logs only from erroneous/slow requests -; (saves API quota for intensive logging) -#appenlight.logging_on_error = false - -; list of additional keywords that should be grabbed from environ object -; can be string with comma separated list of words in lowercase -; (by default client will always send following info: -; 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that -; start with HTTP* this list be extended with additional keywords here -#appenlight.environ_keys_whitelist = - -; list of keywords that should be blanked from request object -; can be string with comma separated list of words in lowercase -; (by default client will always blank keys that contain following words -; 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf' -; this list be extended with additional keywords set here -#appenlight.request_keys_blacklist = - -; list of namespaces that should be ignores when gathering log entries -; can be string with comma separated list of namespaces -; (by default the client ignores own entries: appenlight_client.client) -#appenlight.log_namespace_blacklist = - ; Statsd client config, this is used to send metrics to statsd ; We recommend setting statsd_exported and scrape them using Prometheus #statsd.enabled = false From d00bf18b2dd16024818a601aa9ba421ae63a237b Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 4 Mar 2024 12:54:09 +0100 Subject: [PATCH 032/150] fix(tests): fixed svn tests --- rhodecode/apps/ssh_support/lib/backends/svn.py | 2 +- rhodecode/apps/ssh_support/tests/test_server_svn.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/rhodecode/apps/ssh_support/lib/backends/svn.py b/rhodecode/apps/ssh_support/lib/backends/svn.py index b9273fe8..ca27890f 100644 --- a/rhodecode/apps/ssh_support/lib/backends/svn.py +++ b/rhodecode/apps/ssh_support/lib/backends/svn.py @@ -212,7 +212,7 @@ class SubversionTunnelWrapper(object): first_response = self.get_first_client_response() if not first_response: - return self.fail("Repository name cannot be extracted") + return self.fail(b"Repository name cannot be extracted") url_parts = urllib.parse.urlparse(first_response['url']) diff --git a/rhodecode/apps/ssh_support/tests/test_server_svn.py b/rhodecode/apps/ssh_support/tests/test_server_svn.py index 52bd6ad1..0bbc68a2 100644 --- a/rhodecode/apps/ssh_support/tests/test_server_svn.py +++ b/rhodecode/apps/ssh_support/tests/test_server_svn.py @@ -62,6 +62,7 @@ def svn_server(app): class TestSubversionServer(object): + def test_command(self, svn_server): server = svn_server.create() expected_command = [ From ca042238f99d4cc46773f248078817353b5ecfaf Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 5 Mar 2024 15:36:11 +0100 Subject: [PATCH 033/150] chore(sync-up): synced libs/settings maker from vcsserver --- rhodecode/config/settings_maker.py | 22 ++++++++++++++++++++-- rhodecode/lib/str_utils.py | 14 ++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/rhodecode/config/settings_maker.py b/rhodecode/config/settings_maker.py index 873b1e7c..a82308dd 100644 --- a/rhodecode/config/settings_maker.py +++ b/rhodecode/config/settings_maker.py @@ -23,6 +23,7 @@ import functools import logging import tempfile import logging.config + from rhodecode.lib.type_utils import str2bool, aslist log = logging.getLogger(__name__) @@ -34,13 +35,16 @@ set_keys = { } -class SettingsMaker(object): +class SettingsMaker: def __init__(self, app_settings): self.settings = app_settings @classmethod def _bool_func(cls, input_val): + if isinstance(input_val, bytes): + # decode to str + input_val = input_val.decode('utf8') return str2bool(input_val) @classmethod @@ -61,12 +65,25 @@ class SettingsMaker(object): input_val = input_val.lower() return input_val + @classmethod + def _string_no_quote_func(cls, input_val, lower=True): + """ + Special case string function that detects if value is set to empty quote string + e.g. + + core.binar_dir = "" + """ + + input_val = cls._string_func(input_val, lower=lower) + if input_val in ['""', "''"]: + return '' + @classmethod def _dir_func(cls, input_val, ensure_dir=False, mode=0o755): # ensure we have our dir created if not os.path.isdir(input_val) and ensure_dir: - os.makedirs(input_val, mode=mode) + os.makedirs(input_val, mode=mode, exist_ok=True) if not os.path.isdir(input_val): raise Exception(f'Dir at {input_val} does not exist') @@ -150,6 +167,7 @@ class SettingsMaker(object): 'list:newline': functools.partial(self._list_func, sep='/n'), 'list:spacesep': functools.partial(self._list_func, sep=' '), 'string': functools.partial(self._string_func, lower=lower), + 'string:noquote': functools.partial(self._string_no_quote_func, lower=lower), 'dir': self._dir_func, 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True), 'file': self._file_path_func, diff --git a/rhodecode/lib/str_utils.py b/rhodecode/lib/str_utils.py index a20e427e..78633d2d 100644 --- a/rhodecode/lib/str_utils.py +++ b/rhodecode/lib/str_utils.py @@ -167,3 +167,17 @@ def convert_special_chars(str_) -> str: value = safe_str(str_) converted_value = unidecode(value) return converted_value + + +def splitnewlines(text: bytes): + """ + like splitlines, but only split on newlines. + """ + + lines = [_l + b'\n' for _l in text.split(b'\n')] + if lines: + if lines[-1] == b'\n': + lines.pop() + else: + lines[-1] = lines[-1][:-1] + return lines From 7f994fc993f9fd49fdb1f060aa28f0a43f7c8a55 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 11 Mar 2024 09:56:10 +0100 Subject: [PATCH 034/150] tests(core): made the test flag setting more flexible. We no longer use .ini setting, but relly on env variables, that is now set via pytest env plugin --- pytest.ini | 4 ++++ requirements_test.txt | 6 ++++++ rhodecode/__init__.py | 2 +- rhodecode/config/config_maker.py | 1 - rhodecode/config/environment.py | 3 +-- rhodecode/tests/database/conftest.py | 3 +-- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pytest.ini b/pytest.ini index a5d7bff1..e1bd2486 100644 --- a/pytest.ini +++ b/pytest.ini @@ -21,3 +21,7 @@ markers = skip_backends: Mark tests as skipped for given backends. backends: Mark backends dbs: database markers for running tests for given DB + +env = + RC_TEST=1 + RUN_ENV=test diff --git a/requirements_test.txt b/requirements_test.txt index 72f7f74a..6b09e542 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,6 +11,12 @@ pytest-cov==4.0.0 iniconfig==2.0.0 packaging==23.1 pluggy==1.0.0 +pytest-env==1.1.3 + pytest==7.3.1 + attrs==22.2.0 + iniconfig==2.0.0 + packaging==23.1 + pluggy==1.0.0 pytest-rerunfailures==12.0 pytest-profiling==1.7.0 gprof2dot==2022.7.29 diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py index 541a3418..2d6446aa 100644 --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -87,5 +87,5 @@ __license__ = 'AGPLv3, and Commercial License' __author__ = 'RhodeCode GmbH' __url__ = 'https://code.rhodecode.com' -is_test = False +is_test = os.getenv('RC_TEST') disable_error_handler = False diff --git a/rhodecode/config/config_maker.py b/rhodecode/config/config_maker.py index d27897fe..9587c0b8 100644 --- a/rhodecode/config/config_maker.py +++ b/rhodecode/config/config_maker.py @@ -92,7 +92,6 @@ def sanitize_settings_and_apply_defaults(global_config, settings): # Sanitize generic settings. settings_maker.make_setting('default_encoding', 'UTF-8', parser='list') - settings_maker.make_setting('is_test', False, parser='bool') settings_maker.make_setting('gzip_responses', False, parser='bool') settings_maker.make_setting('startup.import_repos', 'false', parser='bool') diff --git a/rhodecode/config/environment.py b/rhodecode/config/environment.py index 376cad85..20ee0f38 100644 --- a/rhodecode/config/environment.py +++ b/rhodecode/config/environment.py @@ -46,8 +46,7 @@ def load_pyramid_environment(global_config, settings): # If this is a test run we prepare the test environment like # creating a test database, test search index and test repositories. # This has to be done before the database connection is initialized. - if settings['is_test']: - rhodecode.is_test = True + if rhodecode.is_test: rhodecode.disable_error_handler = True from rhodecode import authentication authentication.plugin_default_auth_ttl = 0 diff --git a/rhodecode/tests/database/conftest.py b/rhodecode/tests/database/conftest.py index 26e67da7..7799f23c 100644 --- a/rhodecode/tests/database/conftest.py +++ b/rhodecode/tests/database/conftest.py @@ -86,8 +86,7 @@ class DBBackend(object): _store = os.path.dirname(os.path.abspath(__file__)) _type = None _base_ini_config = [{'app:main': {'vcs.start_server': 'false', - 'startup.import_repos': 'false', - 'is_test': 'False'}}] + 'startup.import_repos': 'false'}}] _db_url = [{'app:main': {'sqlalchemy.db1.url': ''}}] _base_db_name = 'rhodecode_test_db_backend' From d7fd83acb17c21674d573f64eca19c7fd8df47b4 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 11 Mar 2024 10:29:11 +0100 Subject: [PATCH 035/150] chore(deps): bumped pytest related tests --- requirements.txt | 7 ++--- requirements_test.txt | 66 ++++++++++++++++++++----------------------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/requirements.txt b/requirements.txt index 55b03477..a47a61dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -91,7 +91,7 @@ dogpile.cache==1.3.2 formencode==2.1.0 six==1.16.0 gunicorn==21.2.0 - packaging==23.1 + packaging==24.0 gevent==24.2.1 greenlet==3.0.3 zope.event==5.0.0 @@ -119,8 +119,8 @@ markdown==3.4.3 msgpack==1.0.7 mysqlclient==2.1.1 nbconvert==7.7.3 - beautifulsoup4==4.11.2 - soupsieve==2.4 + beautifulsoup4==4.12.3 + soupsieve==2.5 bleach==6.1.0 six==1.16.0 webencodings==0.5.1 @@ -165,7 +165,6 @@ nbconvert==7.7.3 platformdirs==3.10.0 traitlets==5.9.0 traitlets==5.9.0 - packaging==23.1 pandocfilters==1.5.0 pygments==2.15.1 tinycss2==1.2.1 diff --git a/requirements_test.txt b/requirements_test.txt index 6b09e542..dafed0fa 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,49 +1,45 @@ # test related requirements - -cov-core==1.15.0 - coverage==7.2.3 -mock==5.0.2 -py==1.11.0 -pytest-cov==4.0.0 - coverage==7.2.3 - pytest==7.3.1 - attrs==22.2.0 +mock==5.1.0 +pytest-cov==4.1.0 + coverage==7.4.3 + pytest==8.1.1 iniconfig==2.0.0 - packaging==23.1 - pluggy==1.0.0 + packaging==24.0 + pluggy==1.4.0 pytest-env==1.1.3 - pytest==7.3.1 - attrs==22.2.0 + pytest==8.1.1 iniconfig==2.0.0 - packaging==23.1 - pluggy==1.0.0 -pytest-rerunfailures==12.0 + packaging==24.0 + pluggy==1.4.0 pytest-profiling==1.7.0 gprof2dot==2022.7.29 - pytest==7.3.1 - attrs==22.2.0 + pytest==8.1.1 iniconfig==2.0.0 - packaging==23.1 - pluggy==1.0.0 + packaging==24.0 + pluggy==1.4.0 six==1.16.0 -pytest-runner==6.0.0 -pytest-sugar==0.9.7 - packaging==23.1 - pytest==7.3.1 - attrs==22.2.0 +pytest-rerunfailures==13.0 + packaging==24.0 + pytest==8.1.1 iniconfig==2.0.0 - packaging==23.1 - pluggy==1.0.0 - termcolor==2.3.0 -pytest-timeout==2.1.0 - pytest==7.3.1 - attrs==22.2.0 + packaging==24.0 + pluggy==1.4.0 +pytest-runner==6.0.1 +pytest-sugar==1.0.0 + packaging==24.0 + pytest==8.1.1 iniconfig==2.0.0 - packaging==23.1 - pluggy==1.0.0 + packaging==24.0 + pluggy==1.4.0 + termcolor==2.4.0 +pytest-timeout==2.3.1 + pytest==8.1.1 + iniconfig==2.0.0 + packaging==24.0 + pluggy==1.4.0 webtest==3.0.0 - beautifulsoup4==4.11.2 - soupsieve==2.4 + beautifulsoup4==4.12.3 + soupsieve==2.5 waitress==3.0.0 webob==1.8.7 From e0b1f9c055516c143118d48605d01c51002c08ef Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 11 Mar 2024 14:55:48 +0100 Subject: [PATCH 036/150] fix(events): fixed celery based events where it was missing request object --- rhodecode/events/base.py | 21 +++++++++++++++------ rhodecode/events/repo.py | 4 ++-- rhodecode/lib/auth.py | 2 +- rhodecode/lib/celerylib/tasks.py | 1 + rhodecode/lib/hooks_utils.py | 2 +- rhodecode/lib/pyramid_utils.py | 9 +++++++++ rhodecode/model/repo.py | 2 +- 7 files changed, 30 insertions(+), 11 deletions(-) diff --git a/rhodecode/events/base.py b/rhodecode/events/base.py index ec19fdbb..314866b6 100644 --- a/rhodecode/events/base.py +++ b/rhodecode/events/base.py @@ -15,13 +15,14 @@ # This program is dual-licensed. If you wish to learn more about the # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ + import logging import datetime -import typing from zope.cachedescriptors.property import Lazy as LazyProperty -from pyramid.threadlocal import get_current_request +from rhodecode.lib.pyramid_utils import get_current_request +from rhodecode.lib.auth import AuthUser from rhodecode.lib.utils2 import AttributeDict @@ -41,8 +42,9 @@ class RhodecodeEvent(object): name = "RhodeCodeEvent" no_url_set = '' - def __init__(self, request=None): + def __init__(self, request=None, actor=None): self._request = request + self._actor = actor self.utc_timestamp = datetime.datetime.utcnow() def __repr__(self): @@ -72,16 +74,22 @@ class RhodecodeEvent(object): @property def actor(self): + # if an explicit actor is specified, use this + if self._actor: + return self._actor + auth_user = self.auth_user - if auth_user: + log.debug('Got integration actor: %s', auth_user) + if isinstance(auth_user, AuthUser): instance = auth_user.get_instance() + # we can't find this DB user... if not instance: return AttributeDict(dict( username=auth_user.username, user_id=auth_user.user_id, )) - return instance - + elif auth_user: + return auth_user return SYSTEM_USER @property @@ -129,3 +137,4 @@ class FtsBuild(RhodecodeEvent): """ name = 'fts-build' display_name = 'Start FTS Build' + diff --git a/rhodecode/events/repo.py b/rhodecode/events/repo.py index 8d859439..3794dd45 100644 --- a/rhodecode/events/repo.py +++ b/rhodecode/events/repo.py @@ -156,11 +156,11 @@ class RepoEvent(RhodeCodeIntegrationEvent): Base class for events acting on a repository. """ - def __init__(self, repo): + def __init__(self, repo, actor=None): """ :param repo: a :class:`Repository` instance """ - super().__init__() + super().__init__(actor=actor) self.repo = repo def as_dict(self): diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py index b694db41..34ec7b40 100644 --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -1688,7 +1688,7 @@ def get_csrf_token(session, force_new=False, save_if_missing=True): def get_request(perm_class_instance): - from pyramid.threadlocal import get_current_request + from rhodecode.lib.pyramid_utils import get_current_request pyramid_request = get_current_request() return pyramid_request diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py index 1fe90dbf..e3117e80 100644 --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -193,6 +193,7 @@ def create_repo(form_data, cur_user): enable_downloads=enable_downloads, state=state ) + Session().commit() # now create this repo on Filesystem diff --git a/rhodecode/lib/hooks_utils.py b/rhodecode/lib/hooks_utils.py index 42a45a95..9aeaa5c6 100644 --- a/rhodecode/lib/hooks_utils.py +++ b/rhodecode/lib/hooks_utils.py @@ -17,7 +17,6 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import webob -from pyramid.threadlocal import get_current_request from rhodecode import events from rhodecode.lib import hooks_base @@ -33,6 +32,7 @@ def _supports_repo_type(repo_type): def _get_vcs_operation_context(username, repo_name, repo_type, action): # NOTE(dan): import loop from rhodecode.lib.base import vcs_operation_context + from rhodecode.lib.pyramid_utils import get_current_request check_locking = action in ('pull', 'push') diff --git a/rhodecode/lib/pyramid_utils.py b/rhodecode/lib/pyramid_utils.py index 51aa1816..a69d03ec 100644 --- a/rhodecode/lib/pyramid_utils.py +++ b/rhodecode/lib/pyramid_utils.py @@ -23,6 +23,7 @@ import configparser from rhodecode.lib.config_utils import get_config from pyramid.paster import bootstrap as pyramid_bootstrap, setup_logging # pragma: no cover +from pyramid.threadlocal import get_current_request as pyramid_current_request def bootstrap(config_uri, options=None, env=None): @@ -46,3 +47,11 @@ def bootstrap(config_uri, options=None, env=None): 'ip_addr': '127.0.0.1'}) return pyramid_bootstrap(config_uri, request=request, options=options) + +def get_current_request(): + pyramid_req = pyramid_current_request() + if not pyramid_req: + # maybe we're in celery context and need to get the PYRAMID_REQUEST + from rhodecode.lib.celerylib.loader import celery_app + pyramid_req = celery_app.conf['PYRAMID_REQUEST'] + return pyramid_req diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index e784833c..0d7bd6c1 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -608,7 +608,7 @@ class RepoModel(BaseModel): # we need to flush here, in order to check if database won't # throw any exceptions, create filesystem dirs at the very end self.sa.flush() - events.trigger(events.RepoCreateEvent(new_repo)) + events.trigger(events.RepoCreateEvent(new_repo, actor=owner)) return new_repo except Exception: From 684e1aaeeb77f8dc44025d68c70aec5d62e7c85b Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 11 Mar 2024 15:08:11 +0100 Subject: [PATCH 037/150] fix(imports): fixed circular import problem --- rhodecode/events/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rhodecode/events/base.py b/rhodecode/events/base.py index 314866b6..9496ed79 100644 --- a/rhodecode/events/base.py +++ b/rhodecode/events/base.py @@ -22,7 +22,6 @@ import datetime from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode.lib.pyramid_utils import get_current_request -from rhodecode.lib.auth import AuthUser from rhodecode.lib.utils2 import AttributeDict @@ -74,6 +73,8 @@ class RhodecodeEvent(object): @property def actor(self): + from rhodecode.lib.auth import AuthUser + # if an explicit actor is specified, use this if self._actor: return self._actor From 0e4d7ebf22be6800a242b052fe6688b1033f7289 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 12 Mar 2024 08:02:48 +0100 Subject: [PATCH 038/150] chore(configs): remove is_test flag which is deprecated --- rhodecode/tests/rhodecode.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/rhodecode/tests/rhodecode.ini b/rhodecode/tests/rhodecode.ini index f01777ef..931ec9c4 100644 --- a/rhodecode/tests/rhodecode.ini +++ b/rhodecode/tests/rhodecode.ini @@ -66,7 +66,6 @@ prefix = / ;can be overridden by ;export RC_CACHE_REPO_OBJECT_BACKEND=foo -is_test = True use = egg:rhodecode-enterprise-ce ; enable proxy prefix middleware, defined above From b34d42bb36b0a77687e4636e6858f187267ddc9f Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 12 Mar 2024 09:52:52 +0100 Subject: [PATCH 039/150] chore(deps): bumped redis==5.0.3 --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a47a61dd..b0674ec1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -256,7 +256,8 @@ python3-saml==1.15.0 xmlsec==1.3.13 lxml==4.9.3 pyyaml==6.0.1 -redis==5.0.1 +redis==5.0.3 + async-timeout==4.0.3 regex==2022.10.31 routes==2.5.1 repoze.lru==0.7 From eef381dc5377ee80ab119368546000a3182245e5 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 12 Mar 2024 09:53:09 +0100 Subject: [PATCH 040/150] chore(deps): bumped msgpack==1.0.8 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b0674ec1..2ec36024 100644 --- a/requirements.txt +++ b/requirements.txt @@ -116,7 +116,7 @@ ipython==8.14.0 pure-eval==0.2.2 traitlets==5.9.0 markdown==3.4.3 -msgpack==1.0.7 +msgpack==1.0.8 mysqlclient==2.1.1 nbconvert==7.7.3 beautifulsoup4==4.12.3 From a9c2bf237b9f7daafa821db310ca18fc7e005ca3 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 12 Mar 2024 09:53:25 +0100 Subject: [PATCH 041/150] chore(deps): bumped sqlalchemy==1.4.52 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2ec36024..21393c0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ alembic==1.12.1 mako==1.2.4 markupsafe==2.1.2 - sqlalchemy==1.4.51 + sqlalchemy==1.4.52 greenlet==3.0.3 typing_extensions==4.9.0 async-timeout==4.0.3 @@ -269,7 +269,7 @@ sshpubkeys==3.3.1 pycparser==2.21 ecdsa==0.18.0 six==1.16.0 -sqlalchemy==1.4.51 +sqlalchemy==1.4.52 greenlet==3.0.3 typing_extensions==4.9.0 supervisor==4.2.5 From f8bdbc4c723d52112891de77aa04202fc550b65c Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 12 Mar 2024 09:53:36 +0100 Subject: [PATCH 042/150] chore(deps): bumped alembic==1.13.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 21393c0c..a5820049 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]' -alembic==1.12.1 +alembic==1.13.1 mako==1.2.4 markupsafe==2.1.2 sqlalchemy==1.4.52 From 15fd720c8945b549bfa69be969a5715bd4a96604 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 12 Mar 2024 10:08:17 +0100 Subject: [PATCH 043/150] fix(tests): fixed db migration tests after introduction of RC_TEST flag --- rhodecode/__init__.py | 2 +- rhodecode/tests/database/conftest.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py index 2d6446aa..82a7f06a 100644 --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -87,5 +87,5 @@ __license__ = 'AGPLv3, and Commercial License' __author__ = 'RhodeCode GmbH' __url__ = 'https://code.rhodecode.com' -is_test = os.getenv('RC_TEST') +is_test = os.getenv('RC_TEST', '0') == '1' disable_error_handler = False diff --git a/rhodecode/tests/database/conftest.py b/rhodecode/tests/database/conftest.py index 7799f23c..0bd2c0b3 100644 --- a/rhodecode/tests/database/conftest.py +++ b/rhodecode/tests/database/conftest.py @@ -89,6 +89,7 @@ class DBBackend(object): 'startup.import_repos': 'false'}}] _db_url = [{'app:main': {'sqlalchemy.db1.url': ''}}] _base_db_name = 'rhodecode_test_db_backend' + std_env = {'RC_TEST': '0'} def __init__( self, config_file, db_name=None, basetemp=None, @@ -134,13 +135,15 @@ class DBBackend(object): """ command = cmd + ' ' + ' '.join(args) - sys.stdout.write(command) + sys.stdout.write(f'CMD: {command}') # Tell Python to use UTF-8 encoding out stdout _env = os.environ.copy() _env['PYTHONIOENCODING'] = 'UTF-8' + _env.update(self.std_env) if env: _env.update(env) + self.p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, env=_env) self.stdout, self.stderr = self.p.communicate() stdout_str = safe_str(self.stdout) From 14e425bce62c39e182eea9a0703c08594c4537d9 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 13 Mar 2024 19:46:19 +0100 Subject: [PATCH 044/150] fix(user-models): added extra protection against model username changes that would create duplicates --- rhodecode/apps/my_account/views/my_account.py | 1 + rhodecode/lib/exceptions.py | 4 ++++ rhodecode/model/user.py | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/rhodecode/apps/my_account/views/my_account.py b/rhodecode/apps/my_account/views/my_account.py index 94865969..6b7f184f 100644 --- a/rhodecode/apps/my_account/views/my_account.py +++ b/rhodecode/apps/my_account/views/my_account.py @@ -136,6 +136,7 @@ class MyAccountView(BaseAppView, DataGridAppView): except forms.ValidationFailure as e: c.form = e return self._get_template_context(c) + except Exception: log.exception("Exception updating user") h.flash(_('Error occurred during update of user'), diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py index 5556b242..0b4a3ef2 100644 --- a/rhodecode/lib/exceptions.py +++ b/rhodecode/lib/exceptions.py @@ -144,6 +144,10 @@ class NotAllowedToCreateUserError(Exception): pass +class DuplicateUpdateUserError(Exception): + pass + + class RepositoryCreationError(Exception): pass diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py index 4ddb2292..18e33254 100644 --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -37,7 +37,7 @@ from rhodecode.lib.str_utils import safe_str from rhodecode.lib.exceptions import ( DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, UserOwnsUserGroupsException, NotAllowedToCreateUserError, - UserOwnsPullRequestsException, UserOwnsArtifactsException) + UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError) from rhodecode.lib.caching_query import FromCache from rhodecode.model import BaseModel from rhodecode.model.db import ( @@ -308,6 +308,10 @@ class UserModel(BaseModel): log.debug('Checking for existing account in RhodeCode ' 'database with user_id `%s` ', updating_user_id) user = User.get(updating_user_id) + # now also validate if USERNAME belongs to potentially other user + maybe_other_user = User.get_by_username(username, case_insensitive=True) + if maybe_other_user and maybe_other_user.user_id != updating_user_id: + raise DuplicateUpdateUserError(f'different user exists with the {username} username') else: log.debug('Checking for existing account in RhodeCode ' 'database with username `%s` ', username) From d684cf1f5d9ef505abc7cc139f9d9bf44d1b3d76 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 13 Mar 2024 19:46:50 +0100 Subject: [PATCH 045/150] feat(forms): user profile form would detect nicely duplicates and show a form error. --- rhodecode/model/validation_schema/schemas/user_schema.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rhodecode/model/validation_schema/schemas/user_schema.py b/rhodecode/model/validation_schema/schemas/user_schema.py index 8ec241f1..0431e414 100644 --- a/rhodecode/model/validation_schema/schemas/user_schema.py +++ b/rhodecode/model/validation_schema/schemas/user_schema.py @@ -64,6 +64,7 @@ class ChangePasswordSchema(colander.Schema): @colander.deferred def deferred_username_validator(node, kw): + old_username = kw.get('username') def name_validator(node, value): msg = _( @@ -74,6 +75,11 @@ def deferred_username_validator(node, kw): if not re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value): raise colander.Invalid(node, msg) + if value != old_username: + existing_user = User.get_by_username(value, case_insensitive=True) + if existing_user: + raise colander.Invalid(node, 'Username is already taken') + return name_validator From 0f1fbf4ccc79eb699752df27001c9ec605664962 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Fri, 29 Mar 2024 10:43:02 +0100 Subject: [PATCH 046/150] docs: fixed config files path for editing settings... --- docs/admin/lab-settings.rst | 2 +- docs/admin/sec-x-frame.rst | 2 +- docs/admin/sec-your-server.rst | 2 +- docs/admin/system-overview.rst | 2 +- docs/admin/system_admin/admin-tricks.rst | 2 +- docs/admin/system_admin/config-files-overview.rst | 2 +- docs/admin/system_admin/enable-debug.rst | 4 ++-- docs/admin/system_admin/svn-http.rst | 2 +- docs/admin/system_admin/tuning/tuning-change-encoding.rst | 2 +- docs/admin/system_admin/tuning/tuning-hg-auth-loop.rst | 2 +- .../tuning/tuning-scale-horizontally-cluster.rst | 8 ++++---- .../tuning/tuning-user-sessions-performance.rst | 4 ++-- docs/admin/system_admin/vcs-server.rst | 2 +- docs/api/api.rst | 2 +- docs/auth/ssh-connection.rst | 4 ++-- docs/install/setup-email.rst | 2 +- docs/tutorials/multi-instance-setup.rst | 2 +- 17 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/admin/lab-settings.rst b/docs/admin/lab-settings.rst index 4f7e9010..e1eb14ac 100644 --- a/docs/admin/lab-settings.rst +++ b/docs/admin/lab-settings.rst @@ -8,7 +8,7 @@ level of support to optimize for production scenarios. To enable lab settings, use the following instructions: 1. Open the |RCE| configuration file, - :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` + :file:`config/_shared/rhodecode.ini` 2. Add the following configuration option in the ``[app:main]`` section. diff --git a/docs/admin/sec-x-frame.rst b/docs/admin/sec-x-frame.rst index e894e5c5..da6829e4 100644 --- a/docs/admin/sec-x-frame.rst +++ b/docs/admin/sec-x-frame.rst @@ -42,7 +42,7 @@ information see the :ref:`apache-ws-ref` section. |RCE| can also be configured to force strict *https* connections and Strict Transport Security. To set this, configure the following options to ``true`` -in the :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. +in the :file:`config/_shared/rhodecode.ini` file. .. code-block:: ini diff --git a/docs/admin/sec-your-server.rst b/docs/admin/sec-your-server.rst index b8808c38..95a91d26 100644 --- a/docs/admin/sec-your-server.rst +++ b/docs/admin/sec-your-server.rst @@ -83,7 +83,7 @@ see the `OpenSSL PKI tutorial`_ site, or this `Cloudflare PKI post`_. If the network you are running is SSL/TLS encrypted, you can configure |RCE| to always use secure connections using the ``force_https`` and ``use_htsts`` -options in the :file:`/home/user/.rccontrol/instance-id/rhodecode.ini` file. +options in the :file:`config/_shared/rhodecode.ini` file. For more details, see the :ref:`x-frame` section. FireWalls and Ports diff --git a/docs/admin/system-overview.rst b/docs/admin/system-overview.rst index d79310ef..b4f670f0 100644 --- a/docs/admin/system-overview.rst +++ b/docs/admin/system-overview.rst @@ -78,7 +78,7 @@ For example: Configuration Files ------------------- -* :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` +* :file:`config/_shared/rhodecode.ini` * :file:`/home/{user}/.rccontrol/{instance-id}/search_mapping.ini` * :file:`/home/{user}/.rccontrol/{vcsserver-id}/vcsserver.ini` * :file:`/home/{user}/.rccontrol/supervisor/supervisord.ini` diff --git a/docs/admin/system_admin/admin-tricks.rst b/docs/admin/system_admin/admin-tricks.rst index 7c6bb986..bae08bfd 100644 --- a/docs/admin/system_admin/admin-tricks.rst +++ b/docs/admin/system_admin/admin-tricks.rst @@ -188,7 +188,7 @@ Changing Default Language ^^^^^^^^^^^^^^^^^^^^^^^^^ To change the default language of a |RCE| instance, change the language code -in the :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. To +in the :file:`config/_shared/rhodecode.ini` file. To do this, use the following steps. 1. Open the :file:`rhodecode.ini` file and set the required language code. diff --git a/docs/admin/system_admin/config-files-overview.rst b/docs/admin/system_admin/config-files-overview.rst index 3f9a20b7..b151c07a 100644 --- a/docs/admin/system_admin/config-files-overview.rst +++ b/docs/admin/system_admin/config-files-overview.rst @@ -11,7 +11,7 @@ sections. \- **rhodecode.ini** Default location: - :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` + :file:`config/_shared/rhodecode.ini` This is the main |RCE| configuration file and controls much of its default behaviour. It is also used to configure certain customer diff --git a/docs/admin/system_admin/enable-debug.rst b/docs/admin/system_admin/enable-debug.rst index f688b72d..07779d99 100644 --- a/docs/admin/system_admin/enable-debug.rst +++ b/docs/admin/system_admin/enable-debug.rst @@ -14,7 +14,7 @@ track particular user logs only, and exclude other concurrent ones simply grep by `req_id` uuid which you'll have to find for the individual request. To enable debug mode on a |RCE| instance you need to set the debug property -in the :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. To +in the :file:`config/_shared/rhodecode.ini` file. To do this, use the following steps 1. Open the file and set the ``debug`` line to ``true`` @@ -38,7 +38,7 @@ Debug and Logging Configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Further debugging and logging settings can also be set in the -:file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. +:file:`config/_shared/rhodecode.ini` file. In the logging section, the various packages that run with |RCE| can have different debug levels set. If you want to increase the logging level change diff --git a/docs/admin/system_admin/svn-http.rst b/docs/admin/system_admin/svn-http.rst index 1fbed908..cb9041bd 100644 --- a/docs/admin/system_admin/svn-http.rst +++ b/docs/admin/system_admin/svn-http.rst @@ -134,7 +134,7 @@ permission issues could occur. To do this edit the ``/etc/apache2/envvars`` :guilabel:`Subversion HTTP Server URL`. 3. Open the |RCE| configuration file, - :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` + :file:`config/_shared/rhodecode.ini` 4. Add the following configuration option in the ``[app:main]`` section if you don't have it yet. diff --git a/docs/admin/system_admin/tuning/tuning-change-encoding.rst b/docs/admin/system_admin/tuning/tuning-change-encoding.rst index f9804678..1409bc1a 100644 --- a/docs/admin/system_admin/tuning/tuning-change-encoding.rst +++ b/docs/admin/system_admin/tuning/tuning-change-encoding.rst @@ -4,7 +4,7 @@ Change Default Encoding ----------------------- |RCE| uses ``utf8`` encoding by default. You can change the default encoding -in the :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. To +in the :file:`config/_shared/rhodecode.ini` file. To change the default encoding used by |RCE|, set a new value for the ``default_encoding``. diff --git a/docs/admin/system_admin/tuning/tuning-hg-auth-loop.rst b/docs/admin/system_admin/tuning/tuning-hg-auth-loop.rst index bfd61d1c..be85331b 100644 --- a/docs/admin/system_admin/tuning/tuning-hg-auth-loop.rst +++ b/docs/admin/system_admin/tuning/tuning-hg-auth-loop.rst @@ -7,7 +7,7 @@ When using external authentication tools such as LDAP with |hg|, a password retry loop in |hg| can result in users being locked out due to too many failed password attempts. To prevent this from happening, add the following setting to your -:file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file, in the +:file:`config/_shared/rhodecode.ini` file, in the ``[app:main]`` section. diff --git a/docs/admin/system_admin/tuning/tuning-scale-horizontally-cluster.rst b/docs/admin/system_admin/tuning/tuning-scale-horizontally-cluster.rst index bf967b9f..4f6d55b4 100644 --- a/docs/admin/system_admin/tuning/tuning-scale-horizontally-cluster.rst +++ b/docs/admin/system_admin/tuning/tuning-scale-horizontally-cluster.rst @@ -100,7 +100,7 @@ Each one should already connect to shared database during installation. 1) Assuming our final url will be http://rc-node-1, Configure `instances_id`, `app.base_url` -a) On **rc-node-2** find the following settings and edit :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` +a) On **rc-node-2** find the following settings and edit :file:`config/_shared/rhodecode.ini` .. code-block:: ini @@ -109,7 +109,7 @@ a) On **rc-node-2** find the following settings and edit :file:`/home/{user}/.rc app.base_url = http://rc-node-1 -b) On **rc-node-3** find the following settings and edit :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` +b) On **rc-node-3** find the following settings and edit :file:`config/_shared/rhodecode.ini` .. code-block:: ini @@ -121,7 +121,7 @@ b) On **rc-node-3** find the following settings and edit :file:`/home/{user}/.rc 2) Configure `User Session` to use a shared database. Example config that should be changed on both **rc-node-2** and **rc-node-3** . - Edit :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` + Edit :file:`config/_shared/rhodecode.ini` .. code-block:: ini @@ -163,7 +163,7 @@ persistent sessions across nodes. Please generate other one then in this example 4) Use shared exception store. Example config that should be changed on both **rc-node-2** and **rc-node-3**, and also for VCSServer. - Edit :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` and + Edit :file:`config/_shared/rhodecode.ini` and :file:`/home/{user}/.rccontrol/{vcsserver-instance-id}/vcsserver.ini` and add/change following setting. diff --git a/docs/admin/system_admin/tuning/tuning-user-sessions-performance.rst b/docs/admin/system_admin/tuning/tuning-user-sessions-performance.rst index de0036d7..dd3879e0 100644 --- a/docs/admin/system_admin/tuning/tuning-user-sessions-performance.rst +++ b/docs/admin/system_admin/tuning/tuning-user-sessions-performance.rst @@ -15,7 +15,7 @@ scalability, and maintainability we recommend switching from file-based sessions to database-based user sessions or Redis based sessions. To switch to database-based user sessions uncomment the following section in -your :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. +your :file:`config/_shared/rhodecode.ini` file. .. code-block:: ini @@ -49,7 +49,7 @@ uses, or if required it can be a different one. We recommend to use the same dat To switch to redis-based user sessions uncomment the following section in -your :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. +your :file:`config/_shared/rhodecode.ini` file. .. code-block:: ini diff --git a/docs/admin/system_admin/vcs-server.rst b/docs/admin/system_admin/vcs-server.rst index b0a87068..0850d9f1 100644 --- a/docs/admin/system_admin/vcs-server.rst +++ b/docs/admin/system_admin/vcs-server.rst @@ -52,7 +52,7 @@ To configure a |RCE| instance to use a VCS server, see The following list shows the available options on the |RCE| side of the connection to the VCS Server. The settings are configured per instance in the -:file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. +:file:`config/_shared/rhodecode.ini` file. .. rst-class:: dl-horizontal diff --git a/docs/api/api.rst b/docs/api/api.rst index ae7c491e..5cb976c6 100644 --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -27,7 +27,7 @@ of views that have API access enabled by default. To enable these, edit the |RCE| configuration ``.ini`` file. The default location is: * |RCE| Pre-2.2.7 :file:`root/rhodecode/data/production.ini` -* |RCE| 3.0 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` +* |RCE| 3.0 :file:`config/_shared/rhodecode.ini` To configure the white list, edit this section of the file. In this configuration example, API access is granted to the patch/diff raw file and diff --git a/docs/auth/ssh-connection.rst b/docs/auth/ssh-connection.rst index 7a53b281..721af64d 100644 --- a/docs/auth/ssh-connection.rst +++ b/docs/auth/ssh-connection.rst @@ -87,7 +87,7 @@ following instructions. Hostname is required for the integration to properly set the instance URL. When your hostname is known (e.g https://code.rhodecode.com) please set it - inside :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` + inside :file:`config/_shared/rhodecode.ini` add into `[app:main]` section the following configuration: @@ -111,7 +111,7 @@ following instructions. In case of connection problems please set `ssh.enable_debug_logging = true` inside the SSH configuration of - :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` + :file:`config/_shared/rhodecode.ini` Then add, remove your SSH key and try connecting again. Debug logging will be printed to help find the problems on the server side. diff --git a/docs/install/setup-email.rst b/docs/install/setup-email.rst index ab0190d1..0a81d6e3 100644 --- a/docs/install/setup-email.rst +++ b/docs/install/setup-email.rst @@ -4,7 +4,7 @@ Set up Email ------------ To setup email with your |RCE| instance, open the default -:file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` +:file:`config/_shared/rhodecode.ini` file and uncomment and configure the email section. If it is not there, use the below example to insert it. diff --git a/docs/tutorials/multi-instance-setup.rst b/docs/tutorials/multi-instance-setup.rst index c794031c..e229b7dc 100644 --- a/docs/tutorials/multi-instance-setup.rst +++ b/docs/tutorials/multi-instance-setup.rst @@ -27,7 +27,7 @@ from the same database using the following steps: Once the new instance is installed you need to update the licence token and database connection string in the - :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. + :file:`config/_shared/rhodecode.ini` file. .. code-block:: bash From b6eb01f47ba09f9dc0da2116116bde737e4efbf1 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Fri, 29 Mar 2024 10:54:39 +0100 Subject: [PATCH 047/150] deps(js): drop qr generation lib from JS deps as we don't need it. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 2988f2c8..3c007365 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "moment": "^2.18.1", "mousetrap": "^1.6.1", "polymer-webpack-loader": "^2.0.1", - "qrious": "^4.0.2", "raw-loader": "1.0.0-beta.0", "sticky-sidebar": "3.3.1", "style-loader": "^0.21.0", From d8281bb5c0bcb0e2caa8f4a04732a38acd94ca7c Mon Sep 17 00:00:00 2001 From: Serhii Ilin Date: Thu, 14 Mar 2024 11:18:07 +0200 Subject: [PATCH 048/150] feat(login by email option): added ability to log in with user primary email. Fixes: RCCE-63 --- rhodecode/apps/login/tests/test_login.py | 12 ++++++++++++ rhodecode/apps/login/views.py | 8 ++++---- rhodecode/authentication/base.py | 6 +----- rhodecode/authentication/plugins/auth_rhodecode.py | 2 +- rhodecode/model/db.py | 8 +++++++- rhodecode/model/validators.py | 2 +- rhodecode/templates/login.mako | 4 ++-- 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/rhodecode/apps/login/tests/test_login.py b/rhodecode/apps/login/tests/test_login.py index 94aca8df..739ccfb7 100644 --- a/rhodecode/apps/login/tests/test_login.py +++ b/rhodecode/apps/login/tests/test_login.py @@ -80,6 +80,18 @@ class TestLoginController(object): assert username == 'test_regular' response.mustcontain('logout') + def test_login_with_primary_email(self): + user_email = 'test_regular@mail.com' + response = self.app.post(route_path('login'), + {'username': user_email, + 'password': 'test12'}, status=302) + response = response.follow() + session = response.get_session_from_response() + user = session['rhodecode_user'] + assert user['username'] == user_email.split('@')[0] + assert user['is_authenticated'] + response.mustcontain('logout') + def test_login_regular_forbidden_when_super_admin_restriction(self): from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin with fixture.auth_restriction(self.app._pyramid_registry, diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py index c1269508..8dd93d3b 100644 --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -54,8 +54,8 @@ CaptchaData = collections.namedtuple( 'CaptchaData', 'active, private_key, public_key') -def store_user_in_session(session, username, remember=False): - user = User.get_by_username(username, case_insensitive=True) +def store_user_in_session(session, user_identifier, remember=False): + user = User.get_by_username_or_primary_email(user_identifier) auth_user = AuthUser(user.user_id) auth_user.set_authenticated() cs = auth_user.get_cookie_store() @@ -74,7 +74,7 @@ def store_user_in_session(session, username, remember=False): safe_cs = cs.copy() safe_cs['password'] = '****' log.info('user %s is now authenticated and stored in ' - 'session, session attrs %s', username, safe_cs) + 'session, session attrs %s', user_identifier, safe_cs) # dumps session attrs back to cookie session._update_cookie_out() @@ -181,7 +181,7 @@ class LoginView(BaseAppView): # form checks for username/password, now we're authenticated headers = store_user_in_session( self.session, - username=form_result['username'], + user_identifier=form_result['username'], remember=form_result['remember']) log.debug('Redirecting to "%s" after login.', c.came_from) diff --git a/rhodecode/authentication/base.py b/rhodecode/authentication/base.py index e19800aa..29971248 100644 --- a/rhodecode/authentication/base.py +++ b/rhodecode/authentication/base.py @@ -389,11 +389,7 @@ class RhodeCodeAuthPluginBase(object): log.debug( 'Trying to fetch user `%s` from RhodeCode database', username) if username: - user = User.get_by_username(username) - if not user: - log.debug('User not found, fallback to fetch user in ' - 'case insensitive mode') - user = User.get_by_username(username, case_insensitive=True) + user = User.get_by_username_or_primary_email(username) else: log.debug('provided username:`%s` is empty skipping...', username) if not user: diff --git a/rhodecode/authentication/plugins/auth_rhodecode.py b/rhodecode/authentication/plugins/auth_rhodecode.py index 6a041a46..e819b965 100644 --- a/rhodecode/authentication/plugins/auth_rhodecode.py +++ b/rhodecode/authentication/plugins/auth_rhodecode.py @@ -169,7 +169,7 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase): extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username}) return user_attrs - elif userobj.username == username and password_match: + elif (userobj.username == username or userobj.email == username) and password_match: log.info('user `%s` authenticated correctly', userobj.username, extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username}) return user_attrs diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index 63e095bd..bae7154f 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -35,7 +35,7 @@ import collections from sqlalchemy import ( or_, and_, not_, func, cast, TypeDecorator, event, select, - true, false, null, + true, false, null, union_all, Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column, Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary, Text, Float, PickleType, BigInteger) @@ -953,6 +953,12 @@ class User(Base, BaseModel): return cls.execute(q).scalar_one_or_none() + @classmethod + def get_by_username_or_primary_email(cls, user_identifier): + qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)), + cls.select().where(func.lower(cls.email) == func.lower(user_identifier))) + return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none() + @classmethod def get_by_auth_token(cls, auth_token, cache=False): diff --git a/rhodecode/model/validators.py b/rhodecode/model/validators.py index 67ff9d0b..5236f82b 100644 --- a/rhodecode/model/validators.py +++ b/rhodecode/model/validators.py @@ -432,7 +432,7 @@ def ValidAuth(localizer): if not authenticate(username, password, '', HTTP_TYPE, skip_missing=True): - user = User.get_by_username(username) + user = User.get_by_username_or_primary_email(username) if user and not user.active: log.warning('user %s is disabled', username) msg = M(self, 'disabled_account', state) diff --git a/rhodecode/templates/login.mako b/rhodecode/templates/login.mako index 3c52b0ba..3f5efae3 100644 --- a/rhodecode/templates/login.mako +++ b/rhodecode/templates/login.mako @@ -35,12 +35,12 @@ <%block name="above_login_button" />
${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)} - + ${h.text('username', class_='focus', value=defaults.get('username'))} %if 'username' in errors: ${errors.get('username')} From 31e3c38a18b540f4695be9cf1c15652b1b1996f5 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 3 Apr 2024 13:52:52 +0200 Subject: [PATCH 049/150] chore(deps): bumped paste and pastescript libs for deprecation warnings --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index a5820049..83609b12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -171,8 +171,8 @@ nbconvert==7.7.3 webencodings==0.5.1 traitlets==5.9.0 orjson==3.9.15 -pastescript==3.4.0 - paste==3.7.1 +pastescript==3.5.1 + paste==3.8.0 six==1.16.0 pastedeploy==3.1.0 six==1.16.0 @@ -280,7 +280,7 @@ unidecode==1.3.6 urlobject==2.4.3 waitress==3.0.0 weberror==0.13.1 - paste==3.7.1 + paste==3.8.0 six==1.16.0 pygments==2.15.1 tempita==0.5.2 From 9678665ecde6ff650cec6d798b6f49bfc3e1c106 Mon Sep 17 00:00:00 2001 From: Serhii Ilin Date: Mon, 15 Apr 2024 12:13:03 +0300 Subject: [PATCH 050/150] feat(2fa): Added 2fa option. Fixes: RCCE-65 --- requirements.txt | 1 + rhodecode/apps/_base/__init__.py | 47 +++++- rhodecode/apps/login/__init__.py | 24 +++ rhodecode/apps/login/tests/test_2fa.py | 67 ++++++++ rhodecode/apps/login/views.py | 78 ++++++++- rhodecode/apps/my_account/__init__.py | 28 ++++ rhodecode/apps/my_account/views/my_account.py | 27 ++++ .../authentication/plugins/auth_rhodecode.py | 8 + rhodecode/model/db.py | 91 +++++++++++ rhodecode/model/forms.py | 22 +++ rhodecode/public/js/rhodecode/routes.js | 3 + .../admin/my_account/my_account.mako | 1 + .../admin/my_account/my_account_2fa.mako | 140 ++++++++++++++++ rhodecode/templates/configure_2fa.mako | 153 ++++++++++++++++++ rhodecode/templates/verify_2fa.mako | 37 +++++ rhodecode/tests/routes.py | 1 + 16 files changed, 722 insertions(+), 6 deletions(-) create mode 100644 rhodecode/apps/login/tests/test_2fa.py create mode 100644 rhodecode/templates/admin/my_account/my_account_2fa.mako create mode 100644 rhodecode/templates/configure_2fa.mako create mode 100644 rhodecode/templates/verify_2fa.mako diff --git a/requirements.txt b/requirements.txt index 83609b12..cb04cfc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -290,6 +290,7 @@ webhelpers2==2.1 six==1.16.0 whoosh==2.7.4 zope.cachedescriptors==5.0.0 +qrcode==7.4.2 ## uncomment to add the debug libraries #-r requirements_debug.txt diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py index fc6064b8..e500e6eb 100644 --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -104,6 +104,11 @@ class TemplateArgs(StrictAttributeDict): class BaseAppView(object): + DONT_CHECKOUT_VIEWS = ["channelstream_connect", "ops_ping"] + EXTRA_VIEWS_TO_IGNORE = ['login', 'register', 'logout'] + SETUP_2FA_VIEW = 'setup_2fa' + VERIFY_2FA_VIEW = 'check_2fa' + def __init__(self, context, request): self.request = request self.context = context @@ -117,13 +122,19 @@ class BaseAppView(object): self._rhodecode_user = request.user # auth user self._rhodecode_db_user = self._rhodecode_user.get_instance() + self.user_data = self._rhodecode_db_user.user_data if self._rhodecode_db_user else {} self._maybe_needs_password_change( request.matched_route.name, self._rhodecode_db_user ) + self._maybe_needs_2fa_configuration( + request.matched_route.name, self._rhodecode_db_user + ) + self._maybe_needs_2fa_check( + request.matched_route.name, self._rhodecode_db_user + ) def _maybe_needs_password_change(self, view_name, user_obj): - dont_check_views = ["channelstream_connect", "ops_ping"] - if view_name in dont_check_views: + if view_name in self.DONT_CHECKOUT_VIEWS: return log.debug( @@ -144,7 +155,7 @@ class BaseAppView(object): return now = time.time() - should_change = user_obj.user_data.get("force_password_change") + should_change = self.user_data.get("force_password_change") change_after = safe_int(should_change) or 0 if should_change and now > change_after: log.debug("User %s requires password change", user_obj) @@ -157,6 +168,36 @@ class BaseAppView(object): if view_name not in skip_user_views: raise HTTPFound(self.request.route_path("my_account_password")) + def _maybe_needs_2fa_configuration(self, view_name, user_obj): + if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE: + return + + if not user_obj: + return + + if user_obj.has_forced_2fa and user_obj.extern_type != 'rhodecode': + return + + if (user_obj.has_enabled_2fa + and not self.user_data.get('secret_2fa')) \ + and view_name != self.SETUP_2FA_VIEW: + h.flash( + "You are required to configure 2FA", + "warning", + ignore_duplicate=False, + ) + raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW)) + + def _maybe_needs_2fa_check(self, view_name, user_obj): + if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE: + return + + if not user_obj: + return + + if self.user_data.get('check_2fa') and view_name != self.VERIFY_2FA_VIEW: + raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW)) + def _log_creation_exception(self, e, repo_name): _ = self.request.translate reason = None diff --git a/rhodecode/apps/login/__init__.py b/rhodecode/apps/login/__init__.py index 44f974b8..eed380b1 100644 --- a/rhodecode/apps/login/__init__.py +++ b/rhodecode/apps/login/__init__.py @@ -75,3 +75,27 @@ def includeme(config): LoginView, attr='password_reset_confirmation', route_name='reset_password_confirmation', request_method='GET') + + config.add_route( + name='setup_2fa', + pattern=ADMIN_PREFIX + '/setup_2fa') + config.add_view( + LoginView, + attr='setup_2fa', + route_name='setup_2fa', request_method=['GET', 'POST'], + renderer='rhodecode:templates/configure_2fa.mako') + + config.add_route( + name='check_2fa', + pattern=ADMIN_PREFIX + '/check_2fa') + config.add_view( + LoginView, + attr='verify_2fa', + route_name='check_2fa', request_method='GET', + renderer='rhodecode:templates/verify_2fa.mako') + config.add_view( + LoginView, + attr='verify_2fa', + route_name='check_2fa', request_method='POST', + renderer='rhodecode:templates/verify_2fa.mako') + diff --git a/rhodecode/apps/login/tests/test_2fa.py b/rhodecode/apps/login/tests/test_2fa.py new file mode 100644 index 00000000..7a2ef2f3 --- /dev/null +++ b/rhodecode/apps/login/tests/test_2fa.py @@ -0,0 +1,67 @@ +import pytest + +from rhodecode.model.meta import Session +from rhodecode.tests.fixture import Fixture +from rhodecode.tests.routes import route_path +from rhodecode.model.settings import SettingsModel + +fixture = Fixture() + + +@pytest.mark.usefixtures('app') +class Test2FA(object): + @classmethod + def setup_class(cls): + cls.password = 'valid-one' + + @classmethod + def teardown_class(cls): + SettingsModel().create_or_update_setting('auth_rhodecode_global_2fa', False) + + def test_redirect_to_2fa_setup_if_enabled_for_user(self, user_util): + user = user_util.create_user(password=self.password) + user.has_enabled_2fa = True + self.app.post( + route_path('login'), + {'username': user.username, + 'password': self.password}) + + response = self.app.get('/') + assert response.status_code == 302 + assert response.location.endswith(route_path('setup_2fa')) + + def test_redirect_to_2fa_check_if_2fa_configured(self, user_util): + user = user_util.create_user(password=self.password) + user.has_enabled_2fa = True + user.secret_2fa + Session().add(user) + Session().commit() + self.app.post( + route_path('login'), + {'username': user.username, + 'password': self.password}) + response = self.app.get('/') + assert response.status_code == 302 + assert response.location.endswith(route_path('check_2fa')) + + def test_2fa_recovery_codes_works_only_once(self, user_util): + user = user_util.create_user(password=self.password) + user.has_enabled_2fa = True + user.secret_2fa + recovery_cod_to_check = user.get_2fa_recovery_codes()[0] + Session().add(user) + Session().commit() + self.app.post( + route_path('login'), + {'username': user.username, + 'password': self.password}) + response = self.app.post(route_path('check_2fa'), {'totp': recovery_cod_to_check}) + assert response.status_code == 302 + response = self.app.post(route_path('check_2fa'), {'totp': recovery_cod_to_check}) + response.mustcontain('Code is invalid. Try again!') + + def test_2fa_state_when_forced_by_admin(self, user_util): + user = user_util.create_user(password=self.password) + user.has_enabled_2fa = False + SettingsModel().create_or_update_setting('auth_rhodecode_global_2fa', True) + assert user.has_enabled_2fa diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py index 8dd93d3b..e99deeaf 100644 --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -17,6 +17,9 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import time +import json +import pyotp +import qrcode import collections import datetime import formencode @@ -24,7 +27,11 @@ import formencode.htmlfill import logging import urllib.parse import requests +from io import BytesIO +from base64 import b64encode +from pyramid.renderers import render +from pyramid.response import Response from pyramid.httpexceptions import HTTPFound @@ -35,12 +42,12 @@ from rhodecode.events import UserRegistered, trigger from rhodecode.lib import helpers as h from rhodecode.lib import audit_logger from rhodecode.lib.auth import ( - AuthUser, HasPermissionAnyDecorator, CSRFRequired) + AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous) from rhodecode.lib.base import get_ip_addr from rhodecode.lib.exceptions import UserCreationError from rhodecode.lib.utils2 import safe_str from rhodecode.model.db import User, UserApiKeys -from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm +from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm from rhodecode.model.meta import Session from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model.settings import SettingsModel @@ -179,9 +186,12 @@ class LoginView(BaseAppView): self.session.invalidate() form_result = login_form.to_python(self.request.POST) # form checks for username/password, now we're authenticated + username = form_result['username'] + if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa: + user.update_userdata(check_2fa=True) headers = store_user_in_session( self.session, - user_identifier=form_result['username'], + user_identifier=username, remember=form_result['remember']) log.debug('Redirecting to "%s" after login.', c.came_from) @@ -436,6 +446,8 @@ class LoginView(BaseAppView): return self._get_template_context(c, **template_context) + @LoginRequired() + @NotAnonymous() def password_reset_confirmation(self): self.load_default_context() if self.request.GET and self.request.GET.get('key'): @@ -467,3 +479,63 @@ class LoginView(BaseAppView): return HTTPFound(self.request.route_path('reset_password')) return HTTPFound(self.request.route_path('login')) + + @LoginRequired() + @NotAnonymous() + def setup_2fa(self): + _ = self.request.translate + c = self.load_default_context() + user_instance = self._rhodecode_db_user + form = TOTPForm(_, user_instance)() + render_ctx = {} + if self.request.method == 'POST': + try: + form.to_python(dict(self.request.POST)) + Session().commit() + raise HTTPFound(c.came_from) + except formencode.Invalid as errors: + defaults = errors.value + render_ctx = { + 'errors': errors.error_dict, + 'defaults': defaults, + } + qr = qrcode.QRCode(version=1, box_size=10, border=5) + secret = user_instance.secret_2fa + Session().flush() + recovery_codes = user_instance.get_2fa_recovery_codes() + Session().commit() + qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri( + name=self.request.user.name)) + qr.make(fit=True) + img = qr.make_image(fill_color='black', back_color='white') + buffered = BytesIO() + img.save(buffered) + return self._get_template_context( + c, + qr=b64encode(buffered.getvalue()).decode("utf-8"), + key=secret, recovery_codes=json.dumps(recovery_codes), + codes_viewed=not bool(recovery_codes), + ** render_ctx + ) + + @LoginRequired() + @NotAnonymous() + def verify_2fa(self): + _ = self.request.translate + c = self.load_default_context() + render_ctx = {} + user_instance = self._rhodecode_db_user + totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)() + if self.request.method == 'POST': + try: + totp_form.to_python(dict(self.request.POST)) + user_instance.update_userdata(check_2fa=False) + Session().commit() + raise HTTPFound(c.came_from) + except formencode.Invalid as errors: + defaults = errors.value + render_ctx = { + 'errors': errors.error_dict, + 'defaults': defaults, + } + return self._get_template_context(c, **render_ctx) diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py index 5ad11ab5..e8dd97cf 100644 --- a/rhodecode/apps/my_account/__init__.py +++ b/rhodecode/apps/my_account/__init__.py @@ -74,6 +74,34 @@ def includeme(config): route_name='my_account_password_update', request_method='POST', renderer='rhodecode:templates/admin/my_account/my_account.mako') + # my account 2fa + config.add_route( + name='my_account_enable_2fa', + pattern=ADMIN_PREFIX + '/my_account/enable_2fa') + config.add_view( + MyAccountView, + attr='my_account_2fa', + route_name='my_account_enable_2fa', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + + config.add_route( + name='my_account_configure_2fa', + pattern=ADMIN_PREFIX + '/my_account/configure_2fa') + config.add_view( + MyAccountView, + attr='my_account_2fa_configure', + route_name='my_account_configure_2fa', request_method='POST', xhr=True, + renderer='json_ext') + + config.add_route( + name='my_account_regenerate_2fa_recovery_codes', + pattern=ADMIN_PREFIX + '/my_account/regenerate_recovery_codes') + config.add_view( + MyAccountView, + attr='my_account_2fa_regenerate_recovery_codes', + route_name='my_account_regenerate_2fa_recovery_codes', request_method='POST', xhr=True, + renderer='json_ext') + # my account tokens config.add_route( name='my_account_auth_tokens', diff --git a/rhodecode/apps/my_account/views/my_account.py b/rhodecode/apps/my_account/views/my_account.py index 6b7f184f..f17088c9 100644 --- a/rhodecode/apps/my_account/views/my_account.py +++ b/rhodecode/apps/my_account/views/my_account.py @@ -202,6 +202,33 @@ class MyAccountView(BaseAppView, DataGridAppView): raise HTTPFound(self.request.route_path('my_account_password')) + @LoginRequired() + @NotAnonymous() + def my_account_2fa(self): + _ = self.request.translate + c = self.load_default_context() + c.active = '2fa' + from rhodecode.model.settings import SettingsModel + user_instance = self._rhodecode_db_user + locked_by_admin = user_instance.has_forced_2fa + c.state_of_2fa = user_instance.has_enabled_2fa + c.locked_2fa = str2bool(locked_by_admin) + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + def my_account_2fa_configure(self): + state = self.request.POST.get('state') + self._rhodecode_db_user.has_enabled_2fa = state + return {'state_of_2fa': state} + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + def my_account_2fa_regenerate_recovery_codes(self): + return {'recovery_codes': self._rhodecode_db_user.regenerate_2fa_recovery_codes()} + @LoginRequired() @NotAnonymous() def my_account_auth_tokens(self): diff --git a/rhodecode/authentication/plugins/auth_rhodecode.py b/rhodecode/authentication/plugins/auth_rhodecode.py index e819b965..91a64377 100644 --- a/rhodecode/authentication/plugins/auth_rhodecode.py +++ b/rhodecode/authentication/plugins/auth_rhodecode.py @@ -183,6 +183,14 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase): class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase): + global_2fa = colander.SchemaNode( + colander.Bool(), + default=False, + description=_('Force all users to use two factor authentication by enabling this.'), + missing=False, + title=_('Global 2FA'), + widget='bool', + ) auth_restriction_choices = [ (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'), diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index bae7154f..c7330451 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -33,6 +33,7 @@ import functools import traceback import collections +import pyotp from sqlalchemy import ( or_, and_, not_, func, cast, TypeDecorator, event, select, true, false, null, union_all, @@ -51,6 +52,7 @@ from zope.cachedescriptors.property import Lazy as LazyProperty from pyramid.threadlocal import get_current_request from webhelpers2.text import remove_formatting +from rhodecode import ConfigGet from rhodecode.lib.str_utils import safe_bytes from rhodecode.translation import _ from rhodecode.lib.vcs import get_vcs_instance, VCSError @@ -586,6 +588,7 @@ class User(Base, BaseModel): DEFAULT_USER = 'default' DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org' DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}' + RECOVERY_CODES_COUNT = 10 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) username = Column("username", String(255), nullable=True, unique=None, default=None) @@ -793,6 +796,94 @@ class User(Base, BaseModel): Session.commit() return artifact_token.api_key + @hybrid_property + def secret_2fa(self): + if not self.user_data.get('secret_2fa'): + secret = pyotp.random_base32() + self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(secret, enc_key=ENCRYPTION_KEY))) + return secret + return safe_str( + enc_utils.decrypt_value(self.user_data['secret_2fa'], + enc_key=ENCRYPTION_KEY, + strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict', + missing=True) + ) + ) + + def is_totp_valid(self, received_code): + totp = pyotp.TOTP(self.secret_2fa) + return totp.verify(received_code) + + def is_2fa_recovery_code_valid(self, received_code): + encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', []) + recovery_codes = list(map( + lambda x: safe_str( + enc_utils.decrypt_value( + x, + enc_key=ENCRYPTION_KEY, + strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True) + )), + encrypted_recovery_codes)) + if received_code in recovery_codes: + encrypted_recovery_codes.pop(recovery_codes.index(received_code)) + self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes) + return True + return False + + @hybrid_property + def has_forced_2fa(self): + """ + Checks if 2fa was forced for ALL users (including current one) + """ + from rhodecode.model.settings import SettingsModel + # So now we're supporting only auth_rhodecode_global_2f + if value := SettingsModel().get_setting_by_name('auth_rhodecode_global_2fa'): + return value.app_settings_value + return False + + @hybrid_property + def has_enabled_2fa(self): + """ + Checks if 2fa was enabled by user + """ + if value := self.has_forced_2fa: + return value + return self.user_data.get('enabled_2fa', False) + + @has_enabled_2fa.setter + def has_enabled_2fa(self, val): + val = str2bool(val) + self.update_userdata(enabled_2fa=str2bool(val)) + if not val: + self.update_userdata(secret_2fa=None, recovery_codes_2fa=[]) + Session().commit() + + def get_2fa_recovery_codes(self): + """ + Creates 2fa recovery codes + """ + recovery_codes = self.user_data.get('recovery_codes_2fa', []) + encrypted_codes = [] + if not recovery_codes: + for _ in range(self.RECOVERY_CODES_COUNT): + recovery_code = pyotp.random_base32() + recovery_codes.append(recovery_code) + encrypted_codes.append(safe_str(enc_utils.encrypt_value(recovery_code, enc_key=ENCRYPTION_KEY))) + self.update_userdata(recovery_codes_2fa=encrypted_codes) + return recovery_codes + # User should not check the same recovery codes more than once + return [] + + def regenerate_2fa_recovery_codes(self): + """ + Regenerates 2fa recovery codes upon request + """ + self.update_userdata(recovery_codes_2fa=[]) + Session().flush() + new_recovery_codes = self.get_2fa_recovery_codes() + Session().commit() + return new_recovery_codes + @classmethod def get(cls, user_id, cache=False): if not user_id: diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index 96772799..9d625536 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -104,6 +104,28 @@ def LoginForm(localizer): return _LoginForm +def TOTPForm(localizer, user, allow_recovery_code_use=False): + _ = localizer + + class _TOTPForm(formencode.Schema): + allow_extra_fields = True + filter_extra_fields = False + totp = v.Regex(r'^(?:\d{6}|[A-Z0-9]{32})$') + + def to_python(self, value, state=None): + validation_checks = [user.is_totp_valid] + if allow_recovery_code_use: + validation_checks.append(user.is_2fa_recovery_code_valid) + form_data = super().to_python(value, state) + received_code = form_data['totp'] + if not any(map(lambda x: x(received_code), validation_checks)): + error_msg = _('Code is invalid. Try again!') + raise formencode.Invalid(error_msg, v, state, error_dict={'totp': error_msg}) + return True + + return _TOTPForm + + def UserForm(localizer, edit=False, available_languages=None, old_data=None): old_data = old_data or {} available_languages = available_languages or [] diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index 03e43b21..fcb944d4 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -224,6 +224,9 @@ function registerRCRoutes() { pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []); pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []); pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []); + pyroutes.register('check_2fa', '/_admin/check_2fa', []); + pyroutes.register('my_account_configure_2fa', '/_admin/my_account/configure_2fa', []); + pyroutes.register('my_account_regenerate_2fa_recovery_codes', '/_admin/my_account/regenerate_recovery_codes', []); pyroutes.register('my_account_password', '/_admin/my_account/password', []); pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []); pyroutes.register('my_account_perms', '/_admin/my_account/perms', []); diff --git a/rhodecode/templates/admin/my_account/my_account.mako b/rhodecode/templates/admin/my_account/my_account.mako index 9ba03b23..8f45ffb8 100644 --- a/rhodecode/templates/admin/my_account/my_account.mako +++ b/rhodecode/templates/admin/my_account/my_account.mako @@ -28,6 +28,7 @@
  • ${_('Profile')}
  • ${_('Emails')}
  • ${_('Password')}
  • +
  • ${_('2FA')}
  • ${_('Bookmarks')}
  • ${_('Auth Tokens')}
  • ${_('SSH Keys')}
  • diff --git a/rhodecode/templates/admin/my_account/my_account_2fa.mako b/rhodecode/templates/admin/my_account/my_account_2fa.mako new file mode 100644 index 00000000..36c03027 --- /dev/null +++ b/rhodecode/templates/admin/my_account/my_account_2fa.mako @@ -0,0 +1,140 @@ +<%namespace name="base" file="/base/base.mako"/> + +
    +
    +

    ${_('Enable/Disable 2FA for your account')}

    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    + + +
    + % if c.locked_2fa: + ${_('2FA settings cannot be changed here, because 2FA was forced enabled by RhodeCode Administrator.')} + % endif +
    +
    +
    + +
    + +
    +
    +% if c.state_of_2fa: +
    +
    +

    ${_('Regenerate 2FA recovery codes for your account')}

    +
    +
    +
    + + +
    +
    +
    + +
    +% endif + + + diff --git a/rhodecode/templates/configure_2fa.mako b/rhodecode/templates/configure_2fa.mako new file mode 100644 index 00000000..96bb4c6e --- /dev/null +++ b/rhodecode/templates/configure_2fa.mako @@ -0,0 +1,153 @@ +<%inherit file="base/root.mako"/> + +<%def name="title()"> + ${_('Setup authenticator app')} + %if c.rhodecode_name: + · ${h.branding(c.rhodecode_name)} + %endif + + + +
    + + +
    +

    Setup the authenticator app

    +

    Authenticator apps like Google Authenticator, etc. generate one-time passwords that are used as a second factor to verify you identity.

    + + +
    + +

    Use an authenticator app to scan.

    + +

    ${_('Unable to scan?')} ${_('Click here')}

    + + +

    +
    + ${h.secure_form(h.route_path('setup_2fa'), request=request, id='totp_form')} +
    +
    +

    +

    + +
    +

    +

    +

    +
    + ${h.text('totp', class_='form-control', style='width: 40%;')} +
    + %if 'totp' in errors: + ${errors.get('totp')} +
    + %endif +
    +
    + ${h.submit('save',_('Verify'),class_="btn btn-primary", style='width: 40%;', disabled=not codes_viewed)} +
    +
    +
    +

    +
    +
    +
    +
    +
    +
    + + + diff --git a/rhodecode/templates/verify_2fa.mako b/rhodecode/templates/verify_2fa.mako new file mode 100644 index 00000000..c5d2f3d3 --- /dev/null +++ b/rhodecode/templates/verify_2fa.mako @@ -0,0 +1,37 @@ +<%inherit file="/base/root.mako"/> +<%def name="title()"> + ${_('Check 2FA')} + %if c.rhodecode_name: + · ${h.branding(c.rhodecode_name)} + %endif + + +
    +
    + ${h.secure_form(h.route_path('check_2fa'), request=request, id='totp_form')} +
    +
    +

    +

    + +
    +

    +

    +

    +
    + ${h.text('totp', class_="form-control", style='width: 38%;')} +
    + %if 'totp' in errors: + ${errors.get('totp')} +
    + %endif +
    +
    + ${h.submit('save',_('Verify'),class_="btn btn-primary", style='width: 40%;')} +
    +
    +

    +
    +
    +
    +
    diff --git a/rhodecode/tests/routes.py b/rhodecode/tests/routes.py index f74e45ea..a11a9291 100644 --- a/rhodecode/tests/routes.py +++ b/rhodecode/tests/routes.py @@ -106,6 +106,7 @@ def get_url_defs(): + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}", "login": ADMIN_PREFIX + "/login", "logout": ADMIN_PREFIX + "/logout", + "check_2fa": ADMIN_PREFIX + "/check_2fa", "register": ADMIN_PREFIX + "/register", "reset_password": ADMIN_PREFIX + "/password_reset", "reset_password_confirmation": ADMIN_PREFIX + "/password_reset_confirmation", From 0986ef02a25061dda5d9706ecaeb16f32d8678e9 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 16 Apr 2024 13:14:47 +0200 Subject: [PATCH 051/150] 2fa: recovery codes copy --- .../admin/my_account/my_account_2fa.mako | 61 +++++++------------ 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/rhodecode/templates/admin/my_account/my_account_2fa.mako b/rhodecode/templates/admin/my_account/my_account_2fa.mako index 36c03027..52848a97 100644 --- a/rhodecode/templates/admin/my_account/my_account_2fa.mako +++ b/rhodecode/templates/admin/my_account/my_account_2fa.mako @@ -31,14 +31,9 @@
    -
    + % if c.state_of_2fa:
    @@ -54,22 +49,19 @@
    + % endif + - - From 518dd69772ef65c84e117d2728d9b2f78443b6d9 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 16 Apr 2024 14:30:49 +0200 Subject: [PATCH 052/150] fix: added missing templates --- rhodecode/templates/ejs_templates/templates.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rhodecode/templates/ejs_templates/templates.html b/rhodecode/templates/ejs_templates/templates.html index eb16eeb0..636ed6f6 100644 --- a/rhodecode/templates/ejs_templates/templates.html +++ b/rhodecode/templates/ejs_templates/templates.html @@ -269,6 +269,18 @@ They are permanent until deleted, or converted to regular comments.
    <%= submodule_url %>
    + + ##// END OF EJS Templates
    From 70a5f445784c25f32ab51775c34813227171ea6f Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 22 Apr 2024 11:32:01 +0200 Subject: [PATCH 053/150] fix(encryptor): use a failsafe mechanism of detecting old algo for encryption to NOT crash the app when switching to fernet --- rhodecode/lib/enc_utils.py | 6 +++--- rhodecode/lib/encrypt2.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/rhodecode/lib/enc_utils.py b/rhodecode/lib/enc_utils.py index c472f9ea..4280f507 100644 --- a/rhodecode/lib/enc_utils.py +++ b/rhodecode/lib/enc_utils.py @@ -30,15 +30,15 @@ def encrypt_value(value: bytes, enc_key: bytes, algo: str = ''): def decrypt_value(value: bytes, enc_key: bytes, algo: str = '', strict_mode: bool = False): + enc_key = safe_bytes(enc_key) + value = safe_bytes(value) if not algo: # not explicit algo, just use what's set by config - algo = get_default_algo() + algo = Encryptor.detect_enc_algo(value) or get_default_algo() if algo not in ALLOWED_ALGOS: ValueError(f'Bad encryption algorithm, should be {ALLOWED_ALGOS}, got: {algo}') - enc_key = safe_bytes(enc_key) - value = safe_bytes(value) safe = not strict_mode if algo == 'aes': diff --git a/rhodecode/lib/encrypt2.py b/rhodecode/lib/encrypt2.py index c1e7e963..1c6832b0 100644 --- a/rhodecode/lib/encrypt2.py +++ b/rhodecode/lib/encrypt2.py @@ -23,8 +23,21 @@ class InvalidDecryptedValue(str): class Encryptor(object): key_format = b'enc2$salt:{1}$data:{2}' + pref_len = 5 # salt:, data: + @classmethod + def detect_enc_algo(cls, enc_data: bytes): + parts = enc_data.split(b'$', 3) + if len(parts) != 3: + raise ValueError(f'Encrypted Data has invalid format, expected {cls.key_format}, got {parts}') + + if b'enc$aes_hmac$' in enc_data: + return 'aes' + elif b'enc2$salt' in enc_data: + return 'fernet' + return None + def __init__(self, enc_key: bytes): self.enc_key = enc_key From 12af6697b87c64b58d172b111afd892d3da37489 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 22 Apr 2024 14:54:33 +0200 Subject: [PATCH 054/150] shell: remove module level imports --- rhodecode/lib/paster_commands/ishell.py | 9 +++++++-- rhodecode/lib/pyramid_shell/__init__.py | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/rhodecode/lib/paster_commands/ishell.py b/rhodecode/lib/paster_commands/ishell.py index c680736a..b1714074 100644 --- a/rhodecode/lib/paster_commands/ishell.py +++ b/rhodecode/lib/paster_commands/ishell.py @@ -54,8 +54,13 @@ class Command(BasePasterCommand): parser = BasePasterCommand.standard_parser(verbose=True) summary = "Interactive shell" + def import_all_from_module(self, module_name): + import importlib + module = importlib.import_module(module_name) + globals().update({k: v for k, v in module.__dict__.items() if not k.startswith('_')}) + def command(self): - #get SqlAlchemy session + # get SqlAlchemy session self._init_session() # imports, used in ipython shell @@ -64,7 +69,7 @@ class Command(BasePasterCommand): import time import shutil import datetime - from rhodecode.model.db import * + self.import_all_from_module('rhodecode.model.db') try: from IPython import embed diff --git a/rhodecode/lib/pyramid_shell/__init__.py b/rhodecode/lib/pyramid_shell/__init__.py index e7ebac6e..6ab59ad0 100644 --- a/rhodecode/lib/pyramid_shell/__init__.py +++ b/rhodecode/lib/pyramid_shell/__init__.py @@ -41,6 +41,12 @@ or reset some user/system settings. """ +def import_all_from_module(module_name): + import importlib + module = importlib.import_module(module_name) + globals().update({k: v for k, v in module.__dict__.items() if not k.startswith('_')}) + + def ipython_shell_runner(env, help): # imports, used in ipython shell @@ -50,7 +56,7 @@ def ipython_shell_runner(env, help): import shutil import datetime from rhodecode.model import user, user_group, repo, repo_group - from rhodecode.model.db import * + import_all_from_module('rhodecode.model.db') try: import IPython From cd9fe75554d06aeb9b16631d44ac3c7be89614b8 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 23 Apr 2024 15:25:21 +0200 Subject: [PATCH 055/150] fix(caching): fixed problems with Cache query for users. The old way of querying caused the user get query to be always cached, and returning old results even in 2fa forms. The new limited query doesn't cache the user object resolving issues --- rhodecode/apps/my_account/views/my_account.py | 13 ++++-- rhodecode/model/db.py | 45 ++++++++++--------- rhodecode/model/user.py | 11 +++-- .../my_account/my_account_bookmarks.mako | 4 +- rhodecode/templates/base/base.mako | 14 +++--- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/rhodecode/apps/my_account/views/my_account.py b/rhodecode/apps/my_account/views/my_account.py index f17088c9..254be2ba 100644 --- a/rhodecode/apps/my_account/views/my_account.py +++ b/rhodecode/apps/my_account/views/my_account.py @@ -40,7 +40,7 @@ from rhodecode.lib.utils2 import safe_int, md5, str2bool from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model.comment import CommentsModel from rhodecode.model.db import ( - IntegrityError, or_, in_filter_generator, + IntegrityError, or_, in_filter_generator, select, Repository, UserEmailMap, UserApiKeys, UserFollowing, PullRequest, UserBookmark, RepoGroup, ChangesetStatus) from rhodecode.model.meta import Session @@ -511,8 +511,15 @@ class MyAccountView(BaseAppView, DataGridAppView): def my_account_bookmarks(self): c = self.load_default_context() c.active = 'bookmarks' - c.bookmark_items = UserBookmark.get_bookmarks_for_user( - self._rhodecode_db_user.user_id, cache=False) + + user_bookmarks = \ + select(UserBookmark, Repository, RepoGroup) \ + .where(UserBookmark.user_id == self._rhodecode_user.user_id) \ + .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \ + .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \ + .order_by(UserBookmark.position.asc()) + + c.user_bookmark_items = Session().execute(user_bookmarks).all() return self._get_template_context(c) def _process_bookmark_entry(self, entry, user_id): diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index c7330451..e479f99a 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -884,17 +884,6 @@ class User(Base, BaseModel): Session().commit() return new_recovery_codes - @classmethod - def get(cls, user_id, cache=False): - if not user_id: - return - - user = cls.query() - if cache: - user = user.options( - FromCache("sql_cache_short", f"get_users_{user_id}")) - return user.get(user_id) - @classmethod def extra_valid_auth_tokens(cls, user, role=None): tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\ @@ -1021,12 +1010,23 @@ class User(Base, BaseModel): @user_data.setter def user_data(self, val): if not isinstance(val, dict): - raise Exception('user_data must be dict, got %s' % type(val)) + raise Exception(f'user_data must be dict, got {type(val)}') try: self._user_data = safe_bytes(json.dumps(val)) except Exception: log.error(traceback.format_exc()) + @classmethod + def get(cls, user_id, cache=False): + if not user_id: + return + + user = cls.query() + if cache: + user = user.options( + FromCache("sql_cache_short", f"get_users_{user_id}")) + return user.get(user_id) + @classmethod def get_by_username(cls, username, case_insensitive=False, cache=False): @@ -1611,7 +1611,7 @@ class UserGroup(Base, BaseModel): user_group = cls.query() if cache: user_group = user_group.options( - FromCache("sql_cache_short", "get_users_group_%s" % user_group_id)) + FromCache("sql_cache_short", f"get_users_group_{user_group_id}")) return user_group.get(user_group_id) def permissions(self, with_admins=True, with_owner=True, @@ -2005,7 +2005,7 @@ class Repository(Base, BaseModel): if val: return val else: - cache_key = "get_repo_by_name_%s" % _hash_key(repo_name) + cache_key = f"get_repo_by_name_{_hash_key(repo_name)}" q = q.options( FromCache("sql_cache_short", cache_key)) @@ -5643,18 +5643,23 @@ class UserBookmark(Base, BaseModel): @classmethod def get_bookmarks_for_user(cls, user_id, cache=True): - bookmarks = cls.query() \ - .filter(UserBookmark.user_id == user_id) \ - .options(joinedload(UserBookmark.repository)) \ - .options(joinedload(UserBookmark.repository_group)) \ + bookmarks = select( + UserBookmark.title, + UserBookmark.position, + ) \ + .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \ + .add_columns(RepoGroup.group_id, RepoGroup.group_name) \ + .where(UserBookmark.user_id == user_id) \ + .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \ + .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \ .order_by(UserBookmark.position.asc()) if cache: bookmarks = bookmarks.options( - FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id)) + FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks") ) - return bookmarks.all() + return Session().execute(bookmarks).all() def __repr__(self): return f'' diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py index 18e33254..69af24c0 100644 --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -114,6 +114,7 @@ class UserModel(BaseModel): else: user = self.sa.query(User)\ .filter(User.username == username) + if cache: name_key = _hash_key(username) user = user.options( @@ -765,25 +766,29 @@ class UserModel(BaseModel): 'AuthUser: fill data execution based on: ' 'user_id:%s api_key:%s username:%s', user_id, api_key, username) try: + found_with = '' dbuser = None if user_id: dbuser = self.get(user_id) + found_with = 'user_id' elif api_key: dbuser = self.get_by_auth_token(api_key) + found_with = 'auth_token' elif username: dbuser = self.get_by_username(username) + found_with = 'username' if not dbuser: log.warning( - 'Unable to lookup user by id:%s api_key:%s username:%s', - user_id, token_obfuscate(api_key), username) + 'Unable to lookup user by id:%s api_key:%s username:%s, found with: %s', + user_id, token_obfuscate(api_key), username, found_with) return False if not dbuser.active: log.debug('User `%s:%s` is inactive, skipping fill data', username, user_id) return False - log.debug('AuthUser: filling found user:%s data', dbuser) + log.debug('AuthUser: filling found user:%s data, found with: %s', dbuser, found_with) attrs = { 'user_id': dbuser.user_id, diff --git a/rhodecode/templates/admin/my_account/my_account_bookmarks.mako b/rhodecode/templates/admin/my_account/my_account_bookmarks.mako index 5e0a6e8b..559d8593 100644 --- a/rhodecode/templates/admin/my_account/my_account_bookmarks.mako +++ b/rhodecode/templates/admin/my_account/my_account_bookmarks.mako @@ -83,14 +83,14 @@ ## generate always 10 entries - % for item in (c.bookmark_items + [None for i in range(10)])[:10]: + % for item in (c.user_bookmark_items + [None for i in range(10)])[:10]: % if item is None: ## empty placehodlder ${form_item()} % else: ## actual entry - ${form_item(position=item.position, title=item.title, redirect_url=item.redirect_url, repo=item.repository, repo_group=item.repository_group)} + ${form_item(position=item[0].position, title=item[0].title, redirect_url=item[0].redirect_url, repo=item[1], repo_group=item[2])} % endif % endfor diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako index 3b81decc..51a7a0c7 100644 --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -651,26 +651,26 @@ % endif % for item in c.bookmark_items:
  • - % if item.repository: + % if item.repo_id: - % elif item.repository_group: + % elif item.group_id: % else: From 2fcf5e8c05ff6f3bbb74505d3630c44389e9071d Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 23 Apr 2024 15:26:01 +0200 Subject: [PATCH 056/150] fix(gists): fixed old way of querying for gist store --- rhodecode/model/db.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index e479f99a..a3bc23c0 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -4997,10 +4997,9 @@ class Gist(Base, BaseModel): :param cls: """ from rhodecode.model.gist import GIST_STORE_LOC - q = Session().query(RhodeCodeUi)\ - .filter(RhodeCodeUi.ui_key == URL_SEP) - q = q.options(FromCache("sql_cache_short", "repository_repo_path")) - return os.path.join(q.one().ui_value, GIST_STORE_LOC) + from rhodecode.lib.utils import get_rhodecode_repo_store_path + repo_store_path = get_rhodecode_repo_store_path() + return os.path.join(repo_store_path, GIST_STORE_LOC) def get_api_data(self): """ From 337031c7a49c46f7bf0e948b0a087815f45e06d7 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 24 Apr 2024 09:45:36 +0200 Subject: [PATCH 057/150] feat(2fa): refactor logic arround validation/recoverycodes and workflows of configuration of 2fa - recovery codes are shown in 1 place only - save status about view of recovery codes - made the logic of saving states into user_data more explicit and no longer relly on hacky DB transaction logic - turn JS forms into a regular forms --- rhodecode/apps/_base/__init__.py | 6 +- rhodecode/apps/login/tests/test_2fa.py | 6 +- rhodecode/apps/login/views.py | 39 ++-- rhodecode/apps/my_account/__init__.py | 25 ++- rhodecode/apps/my_account/views/my_account.py | 56 +++++- rhodecode/model/db.py | 127 +++++++++---- rhodecode/model/forms.py | 7 +- rhodecode/public/js/rhodecode/routes.js | 9 +- .../admin/my_account/my_account_2fa.mako | 167 ++++++++++-------- rhodecode/templates/configure_2fa.mako | 112 +++--------- rhodecode/templates/verify_2fa.mako | 69 +++++--- 11 files changed, 353 insertions(+), 270 deletions(-) diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py index e500e6eb..2a4f7e68 100644 --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -178,9 +178,7 @@ class BaseAppView(object): if user_obj.has_forced_2fa and user_obj.extern_type != 'rhodecode': return - if (user_obj.has_enabled_2fa - and not self.user_data.get('secret_2fa')) \ - and view_name != self.SETUP_2FA_VIEW: + if user_obj.needs_2fa_configure and view_name != self.SETUP_2FA_VIEW: h.flash( "You are required to configure 2FA", "warning", @@ -195,7 +193,7 @@ class BaseAppView(object): if not user_obj: return - if self.user_data.get('check_2fa') and view_name != self.VERIFY_2FA_VIEW: + if user_obj.has_check_2fa_flag and view_name != self.VERIFY_2FA_VIEW: raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW)) def _log_creation_exception(self, e, repo_name): diff --git a/rhodecode/apps/login/tests/test_2fa.py b/rhodecode/apps/login/tests/test_2fa.py index 7a2ef2f3..024a8148 100644 --- a/rhodecode/apps/login/tests/test_2fa.py +++ b/rhodecode/apps/login/tests/test_2fa.py @@ -33,7 +33,7 @@ class Test2FA(object): def test_redirect_to_2fa_check_if_2fa_configured(self, user_util): user = user_util.create_user(password=self.password) user.has_enabled_2fa = True - user.secret_2fa + user.init_secret_2fa() Session().add(user) Session().commit() self.app.post( @@ -47,8 +47,8 @@ class Test2FA(object): def test_2fa_recovery_codes_works_only_once(self, user_util): user = user_util.create_user(password=self.password) user.has_enabled_2fa = True - user.secret_2fa - recovery_cod_to_check = user.get_2fa_recovery_codes()[0] + user.init_secret_2fa() + recovery_cod_to_check = user.init_2fa_recovery_codes()[0] Session().add(user) Session().commit() self.app.post( diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py index e99deeaf..6d96a72b 100644 --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -188,7 +188,8 @@ class LoginView(BaseAppView): # form checks for username/password, now we're authenticated username = form_result['username'] if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa: - user.update_userdata(check_2fa=True) + user.has_check_2fa_flag = True + headers = store_user_in_session( self.session, user_identifier=username, @@ -489,23 +490,32 @@ class LoginView(BaseAppView): form = TOTPForm(_, user_instance)() render_ctx = {} if self.request.method == 'POST': + post_items = dict(self.request.POST) + try: - form.to_python(dict(self.request.POST)) + form_details = form.to_python(post_items) + secret = form_details['secret_totp'] + + user_instance.init_2fa_recovery_codes(persist=True, force=True) + user_instance.set_2fa_secret(secret) + Session().commit() - raise HTTPFound(c.came_from) + raise HTTPFound(self.request.route_path('my_account_enable_2fa', _query={'show-recovery-codes': 1})) except formencode.Invalid as errors: defaults = errors.value render_ctx = { 'errors': errors.error_dict, 'defaults': defaults, } + + # NOTE: here we DO NOT persist the secret 2FA, since this is only for setup, once a setup is completed + # only then we should persist it + secret = user_instance.init_secret_2fa(persist=False) + + totp_name = f'RhodeCode token ({self.request.user.username})' + qr = qrcode.QRCode(version=1, box_size=10, border=5) - secret = user_instance.secret_2fa - Session().flush() - recovery_codes = user_instance.get_2fa_recovery_codes() - Session().commit() - qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri( - name=self.request.user.name)) + qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name)) qr.make(fit=True) img = qr.make_image(fill_color='black', back_color='white') buffered = BytesIO() @@ -513,8 +523,8 @@ class LoginView(BaseAppView): return self._get_template_context( c, qr=b64encode(buffered.getvalue()).decode("utf-8"), - key=secret, recovery_codes=json.dumps(recovery_codes), - codes_viewed=not bool(recovery_codes), + key=secret, + totp_name=totp_name, ** render_ctx ) @@ -527,9 +537,12 @@ class LoginView(BaseAppView): user_instance = self._rhodecode_db_user totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)() if self.request.method == 'POST': + post_items = dict(self.request.POST) + # NOTE: inject secret, as it's a post configured saved item. + post_items['secret_totp'] = user_instance.get_secret_2fa() try: - totp_form.to_python(dict(self.request.POST)) - user_instance.update_userdata(check_2fa=False) + totp_form.to_python(post_items) + user_instance.has_check_2fa_flag = False Session().commit() raise HTTPFound(c.came_from) except formencode.Invalid as errors: diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py index e8dd97cf..982871a4 100644 --- a/rhodecode/apps/my_account/__init__.py +++ b/rhodecode/apps/my_account/__init__.py @@ -83,24 +83,35 @@ def includeme(config): attr='my_account_2fa', route_name='my_account_enable_2fa', request_method='GET', renderer='rhodecode:templates/admin/my_account/my_account.mako') - + # my account 2fa save config.add_route( - name='my_account_configure_2fa', - pattern=ADMIN_PREFIX + '/my_account/configure_2fa') + name='my_account_enable_2fa_save', + pattern=ADMIN_PREFIX + '/my_account/enable_2fa_save') config.add_view( MyAccountView, - attr='my_account_2fa_configure', - route_name='my_account_configure_2fa', request_method='POST', xhr=True, + attr='my_account_2fa_update', + route_name='my_account_enable_2fa_save', request_method='POST', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + + # my account 2fa recovery code-reset + config.add_route( + name='my_account_show_2fa_recovery_codes', + pattern=ADMIN_PREFIX + '/my_account/recovery_codes') + config.add_view( + MyAccountView, + attr='my_account_2fa_show_recovery_codes', + route_name='my_account_show_2fa_recovery_codes', request_method='POST', xhr=True, renderer='json_ext') + # my account 2fa recovery code-reset config.add_route( name='my_account_regenerate_2fa_recovery_codes', pattern=ADMIN_PREFIX + '/my_account/regenerate_recovery_codes') config.add_view( MyAccountView, attr='my_account_2fa_regenerate_recovery_codes', - route_name='my_account_regenerate_2fa_recovery_codes', request_method='POST', xhr=True, - renderer='json_ext') + route_name='my_account_regenerate_2fa_recovery_codes', request_method='POST', + renderer='rhodecode:templates/admin/my_account/my_account.mako') # my account tokens config.add_route( diff --git a/rhodecode/apps/my_account/views/my_account.py b/rhodecode/apps/my_account/views/my_account.py index 254be2ba..ac3df097 100644 --- a/rhodecode/apps/my_account/views/my_account.py +++ b/rhodecode/apps/my_account/views/my_account.py @@ -16,6 +16,7 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import time import logging import datetime import string @@ -43,6 +44,7 @@ from rhodecode.model.db import ( IntegrityError, or_, in_filter_generator, select, Repository, UserEmailMap, UserApiKeys, UserFollowing, PullRequest, UserBookmark, RepoGroup, ChangesetStatus) +from rhodecode.model.forms import TOTPForm from rhodecode.model.meta import Session from rhodecode.model.pull_request import PullRequestModel from rhodecode.model.user import UserModel @@ -207,27 +209,65 @@ class MyAccountView(BaseAppView, DataGridAppView): def my_account_2fa(self): _ = self.request.translate c = self.load_default_context() - c.active = '2fa' - from rhodecode.model.settings import SettingsModel - user_instance = self._rhodecode_db_user + c.active = '2FA' + user_instance = c.auth_user.get_instance() locked_by_admin = user_instance.has_forced_2fa c.state_of_2fa = user_instance.has_enabled_2fa + c.user_seen_2fa_recovery_codes = user_instance.has_seen_2fa_codes c.locked_2fa = str2bool(locked_by_admin) return self._get_template_context(c) @LoginRequired() @NotAnonymous() @CSRFRequired() - def my_account_2fa_configure(self): - state = self.request.POST.get('state') - self._rhodecode_db_user.has_enabled_2fa = state - return {'state_of_2fa': state} + def my_account_2fa_update(self): + _ = self.request.translate + c = self.load_default_context() + c.active = '2FA' + user_instance = c.auth_user.get_instance() + + state = self.request.POST.get('2fa_status') == '1' + user_instance.has_enabled_2fa = state + user_instance.update_userdata(update_2fa=time.time()) + Session().commit() + h.flash(_("Successfully saved 2FA settings"), category='success') + raise HTTPFound(self.request.route_path('my_account_enable_2fa')) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + def my_account_2fa_show_recovery_codes(self): + c = self.load_default_context() + user_instance = c.auth_user.get_instance() + user_instance.has_seen_2fa_codes = True + Session().commit() + return {'recovery_codes': user_instance.get_2fa_recovery_codes()} @LoginRequired() @NotAnonymous() @CSRFRequired() def my_account_2fa_regenerate_recovery_codes(self): - return {'recovery_codes': self._rhodecode_db_user.regenerate_2fa_recovery_codes()} + _ = self.request.translate + c = self.load_default_context() + user_instance = c.auth_user.get_instance() + + totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)() + + post_items = dict(self.request.POST) + # NOTE: inject secret, as it's a post configured saved item. + post_items['secret_totp'] = user_instance.get_secret_2fa() + try: + totp_form.to_python(post_items) + user_instance.regenerate_2fa_recovery_codes() + Session().commit() + except formencode.Invalid as errors: + h.flash(_("Failed to generate new recovery codes: {}").format(errors), category='error') + raise HTTPFound(self.request.route_path('my_account_enable_2fa')) + except Exception as e: + h.flash(_("Failed to generate new recovery codes: {}").format(e), category='error') + raise HTTPFound(self.request.route_path('my_account_enable_2fa')) + + raise HTTPFound(self.request.route_path('my_account_enable_2fa', _query={'show-recovery-codes': 1})) @LoginRequired() @NotAnonymous() diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index a3bc23c0..9ea58d58 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -796,34 +796,13 @@ class User(Base, BaseModel): Session.commit() return artifact_token.api_key - @hybrid_property - def secret_2fa(self): - if not self.user_data.get('secret_2fa'): - secret = pyotp.random_base32() - self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(secret, enc_key=ENCRYPTION_KEY))) - return secret - return safe_str( - enc_utils.decrypt_value(self.user_data['secret_2fa'], - enc_key=ENCRYPTION_KEY, - strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict', - missing=True) - ) - ) - - def is_totp_valid(self, received_code): - totp = pyotp.TOTP(self.secret_2fa) + def is_totp_valid(self, received_code, secret): + totp = pyotp.TOTP(secret) return totp.verify(received_code) - def is_2fa_recovery_code_valid(self, received_code): + def is_2fa_recovery_code_valid(self, received_code, secret): encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', []) - recovery_codes = list(map( - lambda x: safe_str( - enc_utils.decrypt_value( - x, - enc_key=ENCRYPTION_KEY, - strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True) - )), - encrypted_recovery_codes)) + recovery_codes = self.get_2fa_recovery_codes() if received_code in recovery_codes: encrypted_recovery_codes.pop(recovery_codes.index(received_code)) self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes) @@ -844,7 +823,7 @@ class User(Base, BaseModel): @hybrid_property def has_enabled_2fa(self): """ - Checks if 2fa was enabled by user + Checks if user enabled 2fa """ if value := self.has_forced_2fa: return value @@ -853,34 +832,109 @@ class User(Base, BaseModel): @has_enabled_2fa.setter def has_enabled_2fa(self, val): val = str2bool(val) - self.update_userdata(enabled_2fa=str2bool(val)) + self.update_userdata(enabled_2fa=val) if not val: - self.update_userdata(secret_2fa=None, recovery_codes_2fa=[]) + # NOTE: setting to false we clear the user_data to not store any 2fa artifacts + self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False) Session().commit() - def get_2fa_recovery_codes(self): + @hybrid_property + def has_check_2fa_flag(self): + """ + Check if check 2fa flag is set for this user + """ + value = self.user_data.get('check_2fa', False) + return value + + @has_check_2fa_flag.setter + def has_check_2fa_flag(self, val): + val = str2bool(val) + self.update_userdata(check_2fa=val) + Session().commit() + + @hybrid_property + def has_seen_2fa_codes(self): + """ + get the flag about if user has seen 2fa recovery codes + """ + value = self.user_data.get('recovery_codes_2fa_seen', False) + return value + + @has_seen_2fa_codes.setter + def has_seen_2fa_codes(self, val): + val = str2bool(val) + self.update_userdata(recovery_codes_2fa_seen=val) + Session().commit() + + @hybrid_property + def needs_2fa_configure(self): + """ + Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work. + + Currently this is 2fa enabled and secret exists + """ + if self.has_enabled_2fa: + return not self.user_data.get('secret_2fa') + return False + + def init_2fa_recovery_codes(self, persist=True, force=False): """ Creates 2fa recovery codes """ recovery_codes = self.user_data.get('recovery_codes_2fa', []) encrypted_codes = [] - if not recovery_codes: + if not recovery_codes or force: for _ in range(self.RECOVERY_CODES_COUNT): recovery_code = pyotp.random_base32() recovery_codes.append(recovery_code) - encrypted_codes.append(safe_str(enc_utils.encrypt_value(recovery_code, enc_key=ENCRYPTION_KEY))) - self.update_userdata(recovery_codes_2fa=encrypted_codes) + encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY) + encrypted_codes.append(safe_str(encrypted_code)) + if persist: + self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False) return recovery_codes # User should not check the same recovery codes more than once return [] + def get_2fa_recovery_codes(self): + encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', []) + strict_mode = ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True) + + recovery_codes = list(map( + lambda val: safe_str( + enc_utils.decrypt_value( + val, + enc_key=ENCRYPTION_KEY, + strict_mode=strict_mode + )), + encrypted_recovery_codes)) + return recovery_codes + + def init_secret_2fa(self, persist=True, force=False): + secret_2fa = self.user_data.get('secret_2fa') + if not secret_2fa or force: + secret = pyotp.random_base32() + if persist: + self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY))) + return secret + return '' + + def get_secret_2fa(self) -> str: + secret_2fa = self.user_data['secret_2fa'] + if secret_2fa: + strict_mode = ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True) + return safe_str( + enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY, strict_mode=strict_mode)) + return '' + + def set_2fa_secret(self, value): + encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY) + self.update_userdata(secret_2fa=safe_str(encrypted_value)) + def regenerate_2fa_recovery_codes(self): """ Regenerates 2fa recovery codes upon request """ - self.update_userdata(recovery_codes_2fa=[]) - Session().flush() - new_recovery_codes = self.get_2fa_recovery_codes() + new_recovery_codes = self.init_2fa_recovery_codes(force=True) Session().commit() return new_recovery_codes @@ -5021,8 +5075,7 @@ class Gist(Base, BaseModel): return data def __json__(self): - data = dict( - ) + data = dict() data.update(self.get_api_data()) return data # SCM functions diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index 9d625536..6ceffd53 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -111,6 +111,7 @@ def TOTPForm(localizer, user, allow_recovery_code_use=False): allow_extra_fields = True filter_extra_fields = False totp = v.Regex(r'^(?:\d{6}|[A-Z0-9]{32})$') + secret_totp = v.String() def to_python(self, value, state=None): validation_checks = [user.is_totp_valid] @@ -118,10 +119,12 @@ def TOTPForm(localizer, user, allow_recovery_code_use=False): validation_checks.append(user.is_2fa_recovery_code_valid) form_data = super().to_python(value, state) received_code = form_data['totp'] - if not any(map(lambda x: x(received_code), validation_checks)): + secret = form_data.get('secret_totp') + + if not any(map(lambda func: func(received_code, secret), validation_checks)): error_msg = _('Code is invalid. Try again!') raise formencode.Invalid(error_msg, v, state, error_dict={'totp': error_msg}) - return True + return form_data return _TOTPForm diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index fcb944d4..3b7c6926 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -95,6 +95,7 @@ function registerRCRoutes() { pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []); pyroutes.register('channelstream_proxy', '/_channelstream', []); pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []); + pyroutes.register('check_2fa', '/_admin/check_2fa', []); pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']); pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']); pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']); @@ -218,22 +219,23 @@ function registerRCRoutes() { pyroutes.register('my_account_emails', '/_admin/my_account/emails', []); pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []); pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []); + pyroutes.register('my_account_enable_2fa', '/_admin/my_account/enable_2fa', []); + pyroutes.register('my_account_enable_2fa_save', '/_admin/my_account/enable_2fa_save', []); pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []); pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []); pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']); pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []); pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []); pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []); - pyroutes.register('check_2fa', '/_admin/check_2fa', []); - pyroutes.register('my_account_configure_2fa', '/_admin/my_account/configure_2fa', []); - pyroutes.register('my_account_regenerate_2fa_recovery_codes', '/_admin/my_account/regenerate_recovery_codes', []); pyroutes.register('my_account_password', '/_admin/my_account/password', []); pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []); pyroutes.register('my_account_perms', '/_admin/my_account/perms', []); pyroutes.register('my_account_profile', '/_admin/my_account/profile', []); pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []); pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []); + pyroutes.register('my_account_regenerate_2fa_recovery_codes', '/_admin/my_account/regenerate_recovery_codes', []); pyroutes.register('my_account_repos', '/_admin/my_account/repos', []); + pyroutes.register('my_account_show_2fa_recovery_codes', '/_admin/my_account/recovery_codes', []); pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []); pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []); pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []); @@ -382,6 +384,7 @@ function registerRCRoutes() { pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']); pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']); pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']); + pyroutes.register('setup_2fa', '/_admin/setup_2fa', []); pyroutes.register('store_user_session_value', '/_store_session_attr', []); pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']); pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']); diff --git a/rhodecode/templates/admin/my_account/my_account_2fa.mako b/rhodecode/templates/admin/my_account/my_account_2fa.mako index 52848a97..41e433b5 100644 --- a/rhodecode/templates/admin/my_account/my_account_2fa.mako +++ b/rhodecode/templates/admin/my_account/my_account_2fa.mako @@ -4,6 +4,7 @@

    ${_('Enable/Disable 2FA for your account')}

    + ${h.secure_form(h.route_path('my_account_enable_2fa_save'), request=request)}
    @@ -12,112 +13,122 @@
    - -
    - - -
    % if c.locked_2fa: ${_('2FA settings cannot be changed here, because 2FA was forced enabled by RhodeCode Administrator.')} + + % else: +
    + + + + + +
    % endif +
    + ${h.end_form()} % if c.state_of_2fa: + + +% if not c.user_seen_2fa_recovery_codes: + +
    +
    +

    ${_('2FA Recovery codes')}

    +
    +
    +

    + ${_('You have not seen your 2FA recovery codes yet.')} + ${_('Please save them in a safe place, or you will lose access to your account in case of lost access to authenticator app.')} +

    +
    + ${_('Show recovery codes')} +
    +
    +% endif + + +${h.secure_form(h.route_path('my_account_regenerate_2fa_recovery_codes'), request=request)}

    ${_('Regenerate 2FA recovery codes for your account')}

    - - + + -
    - +${h.end_form()} % endif + diff --git a/rhodecode/templates/configure_2fa.mako b/rhodecode/templates/configure_2fa.mako index 96bb4c6e..6ee98c93 100644 --- a/rhodecode/templates/configure_2fa.mako +++ b/rhodecode/templates/configure_2fa.mako @@ -1,7 +1,7 @@ <%inherit file="base/root.mako"/> <%def name="title()"> - ${_('Setup authenticator app')} + ${_('Setup 2FA')} %if c.rhodecode_name: · ${h.branding(c.rhodecode_name)} %endif @@ -22,31 +22,28 @@
    -

    Setup the authenticator app

    +

    ${_('Setup the authenticator app')}

    +

    Authenticator apps like Google Authenticator, etc. generate one-time passwords that are used as a second factor to verify you identity.

    + ${h.secure_form(h.route_path('setup_2fa'), request=request, id='totp_form')} -

    Use an authenticator app to scan.

    - +

    ${_('Use an authenticator app to scan.')}

    + qr-code +

    ${_('Unable to scan?')} ${_('Click here')}

    - -

    +
    - ${h.secure_form(h.route_path('setup_2fa'), request=request, id='totp_form')} +

    @@ -59,13 +56,13 @@

    ${h.text('totp', class_='form-control', style='width: 40%;')}
    - %if 'totp' in errors: + % if 'totp' in errors: ${errors.get('totp')}
    - %endif + % endif
    - ${h.submit('save',_('Verify'),class_="btn btn-primary", style='width: 40%;', disabled=not codes_viewed)} + ${h.submit('verify_2fa',_('Verify'),class_="btn btn-primary", style='width: 40%;')}
    @@ -73,81 +70,18 @@
    + ${h.end_form()}
    + - - diff --git a/rhodecode/templates/verify_2fa.mako b/rhodecode/templates/verify_2fa.mako index c5d2f3d3..fa235391 100644 --- a/rhodecode/templates/verify_2fa.mako +++ b/rhodecode/templates/verify_2fa.mako @@ -1,37 +1,54 @@ -<%inherit file="/base/root.mako"/> +<%inherit file="base/root.mako"/> + <%def name="title()"> - ${_('Check 2FA')} + ${_('Verify 2FA')} %if c.rhodecode_name: · ${h.branding(c.rhodecode_name)} %endif + -
    -
    - ${h.secure_form(h.route_path('check_2fa'), request=request, id='totp_form')} -
    -
    -

    -

    - +
    + + +
    + + +
    + +
    + ${h.secure_form(h.route_path('check_2fa'), request=request, id='totp_form')} + + ${h.text('totp', class_="form-control")} + %if 'totp' in errors: + ${errors.get('totp')} +
    + %endif +

    ${_('Enter the code from your two-factor authenticator app. If you\'ve lost your device, you can enter one of your recovery codes.')}

    + + ${h.submit('send', _('Verify'), class_="btn sign-in")} +

    + RhodeCode ${c.rhodecode_edition} +

    + ${h.end_form()} +
    +
    + +
    + + + + From e20aa86d2d7e2db911113a406026722e92dd19a8 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 24 Apr 2024 09:49:39 +0200 Subject: [PATCH 058/150] feat(2fa): updates routes names and urls to better reflect actions --- rhodecode/apps/login/views.py | 2 +- rhodecode/apps/my_account/__init__.py | 12 ++++++------ rhodecode/apps/my_account/views/my_account.py | 8 ++++---- rhodecode/public/js/rhodecode/routes.js | 4 ++-- rhodecode/templates/admin/my_account/my_account.mako | 2 +- .../templates/admin/my_account/my_account_2fa.mako | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py index 6d96a72b..e25112fe 100644 --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -500,7 +500,7 @@ class LoginView(BaseAppView): user_instance.set_2fa_secret(secret) Session().commit() - raise HTTPFound(self.request.route_path('my_account_enable_2fa', _query={'show-recovery-codes': 1})) + raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1})) except formencode.Invalid as errors: defaults = errors.value render_ctx = { diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py index 982871a4..754d2677 100644 --- a/rhodecode/apps/my_account/__init__.py +++ b/rhodecode/apps/my_account/__init__.py @@ -76,21 +76,21 @@ def includeme(config): # my account 2fa config.add_route( - name='my_account_enable_2fa', - pattern=ADMIN_PREFIX + '/my_account/enable_2fa') + name='my_account_configure_2fa', + pattern=ADMIN_PREFIX + '/my_account/configure_2fa') config.add_view( MyAccountView, attr='my_account_2fa', - route_name='my_account_enable_2fa', request_method='GET', + route_name='my_account_configure_2fa', request_method='GET', renderer='rhodecode:templates/admin/my_account/my_account.mako') # my account 2fa save config.add_route( - name='my_account_enable_2fa_save', - pattern=ADMIN_PREFIX + '/my_account/enable_2fa_save') + name='my_account_configure_2fa_update', + pattern=ADMIN_PREFIX + '/my_account/configure_2fa_update') config.add_view( MyAccountView, attr='my_account_2fa_update', - route_name='my_account_enable_2fa_save', request_method='POST', + route_name='my_account_configure_2fa_update', request_method='POST', renderer='rhodecode:templates/admin/my_account/my_account.mako') # my account 2fa recovery code-reset diff --git a/rhodecode/apps/my_account/views/my_account.py b/rhodecode/apps/my_account/views/my_account.py index ac3df097..37a39b4a 100644 --- a/rhodecode/apps/my_account/views/my_account.py +++ b/rhodecode/apps/my_account/views/my_account.py @@ -231,7 +231,7 @@ class MyAccountView(BaseAppView, DataGridAppView): user_instance.update_userdata(update_2fa=time.time()) Session().commit() h.flash(_("Successfully saved 2FA settings"), category='success') - raise HTTPFound(self.request.route_path('my_account_enable_2fa')) + raise HTTPFound(self.request.route_path('my_account_configure_2fa')) @LoginRequired() @NotAnonymous() @@ -262,12 +262,12 @@ class MyAccountView(BaseAppView, DataGridAppView): Session().commit() except formencode.Invalid as errors: h.flash(_("Failed to generate new recovery codes: {}").format(errors), category='error') - raise HTTPFound(self.request.route_path('my_account_enable_2fa')) + raise HTTPFound(self.request.route_path('my_account_configure_2fa')) except Exception as e: h.flash(_("Failed to generate new recovery codes: {}").format(e), category='error') - raise HTTPFound(self.request.route_path('my_account_enable_2fa')) + raise HTTPFound(self.request.route_path('my_account_configure_2fa')) - raise HTTPFound(self.request.route_path('my_account_enable_2fa', _query={'show-recovery-codes': 1})) + raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1})) @LoginRequired() @NotAnonymous() diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index 3b7c6926..37ac3ed9 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -215,12 +215,12 @@ function registerRCRoutes() { pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []); pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []); pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []); + pyroutes.register('my_account_configure_2fa', '/_admin/my_account/configure_2fa', []); + pyroutes.register('my_account_configure_2fa_update', '/_admin/my_account/configure_2fa_update', []); pyroutes.register('my_account_edit', '/_admin/my_account/edit', []); pyroutes.register('my_account_emails', '/_admin/my_account/emails', []); pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []); pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []); - pyroutes.register('my_account_enable_2fa', '/_admin/my_account/enable_2fa', []); - pyroutes.register('my_account_enable_2fa_save', '/_admin/my_account/enable_2fa_save', []); pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []); pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []); pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']); diff --git a/rhodecode/templates/admin/my_account/my_account.mako b/rhodecode/templates/admin/my_account/my_account.mako index 8f45ffb8..6bd7b1f8 100644 --- a/rhodecode/templates/admin/my_account/my_account.mako +++ b/rhodecode/templates/admin/my_account/my_account.mako @@ -28,7 +28,7 @@
  • ${_('Profile')}
  • ${_('Emails')}
  • ${_('Password')}
  • -
  • ${_('2FA')}
  • +
  • ${_('2FA')}
  • ${_('Bookmarks')}
  • ${_('Auth Tokens')}
  • ${_('SSH Keys')}
  • diff --git a/rhodecode/templates/admin/my_account/my_account_2fa.mako b/rhodecode/templates/admin/my_account/my_account_2fa.mako index 41e433b5..55ae4eb9 100644 --- a/rhodecode/templates/admin/my_account/my_account_2fa.mako +++ b/rhodecode/templates/admin/my_account/my_account_2fa.mako @@ -4,7 +4,7 @@

    ${_('Enable/Disable 2FA for your account')}

    - ${h.secure_form(h.route_path('my_account_enable_2fa_save'), request=request)} + ${h.secure_form(h.route_path('my_account_configure_2fa_update'), request=request)}
    @@ -50,7 +50,7 @@ ${_('Please save them in a safe place, or you will lose access to your account in case of lost access to authenticator app.')}


    - ${_('Show recovery codes')} + ${_('Show recovery codes')}
    % endif From 566e572fffd9f8735568c8798aa2b506e8812a58 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 24 Apr 2024 10:38:07 +0200 Subject: [PATCH 059/150] fix(tests): fixed 2fa tests and password reset broken by accident --- rhodecode/apps/login/tests/test_login.py | 2 +- rhodecode/apps/login/views.py | 8 +++----- rhodecode/tests/routes.py | 1 + 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/rhodecode/apps/login/tests/test_login.py b/rhodecode/apps/login/tests/test_login.py index 739ccfb7..88ac3877 100644 --- a/rhodecode/apps/login/tests/test_login.py +++ b/rhodecode/apps/login/tests/test_login.py @@ -435,7 +435,7 @@ class TestLoginController(object): 'If such email exists, a password reset link was sent to it.') # BAD KEY - confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey') + confirm_url = route_path('reset_password_confirmation', params={'key': 'badkey'}) response = self.app.get(confirm_url, status=302) assert response.location.endswith(route_path('reset_password')) assert_session_flash(response, 'Given reset token is invalid') diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py index e25112fe..debd9e8a 100644 --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -447,16 +447,14 @@ class LoginView(BaseAppView): return self._get_template_context(c, **template_context) - @LoginRequired() - @NotAnonymous() def password_reset_confirmation(self): self.load_default_context() - if self.request.GET and self.request.GET.get('key'): + + if key := self.request.GET.get('key'): # make this take 2s, to prevent brute forcing. time.sleep(2) - token = AuthTokenModel().get_auth_token( - self.request.GET.get('key')) + token = AuthTokenModel().get_auth_token(key) # verify token is the correct role if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET: diff --git a/rhodecode/tests/routes.py b/rhodecode/tests/routes.py index a11a9291..0d120419 100644 --- a/rhodecode/tests/routes.py +++ b/rhodecode/tests/routes.py @@ -106,6 +106,7 @@ def get_url_defs(): + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}", "login": ADMIN_PREFIX + "/login", "logout": ADMIN_PREFIX + "/logout", + "setup_2fa": ADMIN_PREFIX + "/setup_2fa", "check_2fa": ADMIN_PREFIX + "/check_2fa", "register": ADMIN_PREFIX + "/register", "reset_password": ADMIN_PREFIX + "/password_reset", From a36b846e92ef3190976c6ca5c34607e28547f433 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 24 Apr 2024 11:13:35 +0200 Subject: [PATCH 060/150] fix(2fa): fixed redirect loop in workflow when password reset was done. --- rhodecode/apps/_base/__init__.py | 1 + rhodecode/lib/middleware/vcs.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py index 2a4f7e68..8f0324ab 100644 --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -144,6 +144,7 @@ class BaseAppView(object): skip_user_views = [ "logout", "login", + "check_2fa", "my_account_password", "my_account_password_update", ] diff --git a/rhodecode/lib/middleware/vcs.py b/rhodecode/lib/middleware/vcs.py index 8796bd49..49c636f2 100644 --- a/rhodecode/lib/middleware/vcs.py +++ b/rhodecode/lib/middleware/vcs.py @@ -26,6 +26,7 @@ import urllib.parse from webob.exc import HTTPNotFound import rhodecode +from rhodecode.apps._base import ADMIN_PREFIX from rhodecode.lib.middleware.utils import get_path_info from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT @@ -164,14 +165,18 @@ def detect_vcs_request(environ, backends): # login "_admin/login", + # 2fa + f"{ADMIN_PREFIX}/check_2fa", + f"{ADMIN_PREFIX}/setup_2fa", + # _admin/api is safe too - '_admin/api', + f'{ADMIN_PREFIX}/api', # _admin/gist is safe too - '_admin/gists++', + f'{ADMIN_PREFIX}/gists++', # _admin/my_account is safe too - '_admin/my_account++', + f'{ADMIN_PREFIX}/my_account++', # static files no detection '_static++', @@ -180,11 +185,11 @@ def detect_vcs_request(environ, backends): '_debug_toolbar++', # skip ops ping, status - '_admin/ops/ping', - '_admin/ops/status', + f'{ADMIN_PREFIX}/ops/ping', + f'{ADMIN_PREFIX}/ops/status', # full channelstream connect should be VCS skipped - '_admin/channelstream/connect', + f'{ADMIN_PREFIX}/channelstream/connect', '++/repo_creating_check' ] From d80f6eb5a89c7c6212bd78d0db8271854f0ed42c Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 24 Apr 2024 11:29:09 +0200 Subject: [PATCH 061/150] fix(2fa): fixed case of imports for templates. --- rhodecode/apps/my_account/views/my_account.py | 4 ++-- rhodecode/templates/admin/my_account/my_account.mako | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rhodecode/apps/my_account/views/my_account.py b/rhodecode/apps/my_account/views/my_account.py index 37a39b4a..60edf563 100644 --- a/rhodecode/apps/my_account/views/my_account.py +++ b/rhodecode/apps/my_account/views/my_account.py @@ -209,7 +209,7 @@ class MyAccountView(BaseAppView, DataGridAppView): def my_account_2fa(self): _ = self.request.translate c = self.load_default_context() - c.active = '2FA' + c.active = '2fa' user_instance = c.auth_user.get_instance() locked_by_admin = user_instance.has_forced_2fa c.state_of_2fa = user_instance.has_enabled_2fa @@ -223,7 +223,7 @@ class MyAccountView(BaseAppView, DataGridAppView): def my_account_2fa_update(self): _ = self.request.translate c = self.load_default_context() - c.active = '2FA' + c.active = '2fa' user_instance = c.auth_user.get_instance() state = self.request.POST.get('2fa_status') == '1' diff --git a/rhodecode/templates/admin/my_account/my_account.mako b/rhodecode/templates/admin/my_account/my_account.mako index 6bd7b1f8..ad0fd217 100644 --- a/rhodecode/templates/admin/my_account/my_account.mako +++ b/rhodecode/templates/admin/my_account/my_account.mako @@ -28,7 +28,7 @@
  • ${_('Profile')}
  • ${_('Emails')}
  • ${_('Password')}
  • -
  • ${_('2FA')}
  • +
  • ${_('2FA')}
  • ${_('Bookmarks')}
  • ${_('Auth Tokens')}
  • ${_('SSH Keys')}
  • From 0628f37e75d51ec74d8e7ad7b84d37512b4c07da Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 24 Apr 2024 11:57:40 +0200 Subject: [PATCH 062/150] feat(2fa): improve flash messages on 2fa settings page --- rhodecode/apps/my_account/views/my_account.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rhodecode/apps/my_account/views/my_account.py b/rhodecode/apps/my_account/views/my_account.py index 60edf563..c9d3d171 100644 --- a/rhodecode/apps/my_account/views/my_account.py +++ b/rhodecode/apps/my_account/views/my_account.py @@ -230,7 +230,10 @@ class MyAccountView(BaseAppView, DataGridAppView): user_instance.has_enabled_2fa = state user_instance.update_userdata(update_2fa=time.time()) Session().commit() - h.flash(_("Successfully saved 2FA settings"), category='success') + if state: + h.flash(_("2FA has been successfully enabled"), category='success') + else: + h.flash(_("2FA has been successfully disabled"), category='success') raise HTTPFound(self.request.route_path('my_account_configure_2fa')) @LoginRequired() From aad99df52134085b97b8cdb66f4d3fa775f1ea89 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Wed, 24 Apr 2024 15:22:16 +0200 Subject: [PATCH 063/150] feat(2fa): updated and UI fixes - configure 2fa - setup 2fa - various small improvements --- rhodecode/apps/login/views.py | 7 +- rhodecode/model/db.py | 2 +- rhodecode/templates/configure_2fa.mako | 97 +++++++++++++------------- rhodecode/templates/verify_2fa.mako | 2 +- 4 files changed, 56 insertions(+), 52 deletions(-) diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py index debd9e8a..7140baa6 100644 --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -34,7 +34,7 @@ from pyramid.renderers import render from pyramid.response import Response from pyramid.httpexceptions import HTTPFound - +import rhodecode from rhodecode.apps._base import BaseAppView from rhodecode.authentication.base import authenticate, HTTP_TYPE from rhodecode.authentication.plugins import auth_rhodecode @@ -510,9 +510,10 @@ class LoginView(BaseAppView): # only then we should persist it secret = user_instance.init_secret_2fa(persist=False) - totp_name = f'RhodeCode token ({self.request.user.username})' + instance_name = rhodecode.ConfigGet().get_str('app.base_url', 'rhodecode') + totp_name = f'{instance_name}:{self.request.user.username}' - qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr = qrcode.QRCode(version=1, box_size=5, border=4) qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name)) qr.make(fit=True) img = qr.make_image(fill_color='black', back_color='white') diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index 9ea58d58..4e26633b 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -919,7 +919,7 @@ class User(Base, BaseModel): return '' def get_secret_2fa(self) -> str: - secret_2fa = self.user_data['secret_2fa'] + secret_2fa = self.user_data.get('secret_2fa') if secret_2fa: strict_mode = ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True) return safe_str( diff --git a/rhodecode/templates/configure_2fa.mako b/rhodecode/templates/configure_2fa.mako index 6ee98c93..e538545c 100644 --- a/rhodecode/templates/configure_2fa.mako +++ b/rhodecode/templates/configure_2fa.mako @@ -6,9 +6,11 @@ · ${h.branding(c.rhodecode_name)} %endif + -
    +
    +