##// END OF EJS Templates
merge: Resolved conflicts
andverb -
r5471:df8a724f merge v5.1.0 stable
parent child Browse files
Show More
@@ -0,0 +1,43 b''
1 |RCE| 5.0.1 |RNS|
2 -----------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2024-05-20
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18
19
20 Security
21 ^^^^^^^^
22
23
24
25 Performance
26 ^^^^^^^^^^^
27
28
29
30
31 Fixes
32 ^^^^^
33
34 - Fixed Celery serialization issues
35 - Fixed Celery startup problems signaling
36 - Fixed SVN hooks binary dir paths which in certain scenarios resulted in empty values forbidding hooks to execute
37 - Fixed annotation bug for files without new lines or mixed newlines
38
39
40 Upgrade notes
41 ^^^^^^^^^^^^^
42
43 - RhodeCode 5.0.1 is unscheduled bugfix release to address some of the issues found during 4.X -> 5.X migration
@@ -0,0 +1,39 b''
1 |RCE| 5.0.2 |RNS|
2 -----------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2024-05-29
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18
19
20 Security
21 ^^^^^^^^
22
23
24
25 Performance
26 ^^^^^^^^^^^
27
28
29
30
31 Fixes
32 ^^^^^
33
34 - Fixed problems with saving branch permissions
35
36 Upgrade notes
37 ^^^^^^^^^^^^^
38
39 - RhodeCode 5.0.2 is unscheduled bugfix release to address some of the issues found during 4.X -> 5.X migration
@@ -0,0 +1,39 b''
1 |RCE| 5.0.3 |RNS|
2 -----------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2024-06-17
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18
19
20 Security
21 ^^^^^^^^
22
23
24
25 Performance
26 ^^^^^^^^^^^
27
28
29
30
31 Fixes
32 ^^^^^
33
34 - Fixed problems nested ldap groups
35
36 Upgrade notes
37 ^^^^^^^^^^^^^
38
39 - RhodeCode 5.0.3 is unscheduled bugfix release to address some of the issues found during 4.X -> 5.X migration
@@ -0,0 +1,59 b''
1 |RCE| 5.1.0 |RNS|
2 -----------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2024-07-18
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - We've introduced 2FA for users. Now alongside the external auth 2fa support RhodeCode allows to enable 2FA for users
14 2FA options will be available for each user individually, or enforced via authentication plugins like ldap, or internal.
15 - Email based log-in. RhodeCode now allows to log-in using email as well as username for main authentication type.
16 - Ability to replace a file using web UI. Now one can replace an existing file from the web-ui.
17 - GIT LFS Sync automation. Remote push/pull commands now can also sync GIT LFS objects.
18 - Added ability to remove or close branches from the web ui
19 - Added ability to delete a branch automatically after merging PR for git repositories
20 - Added support for S3 based archive_cache based that allows storing cached archives in S3 compatible object store.
21
22
23 General
24 ^^^^^^^
25
26 - Upgraded all dependency libraries to their latest available versions
27 - Repository storage is no longer controlled via DB settings, but .ini file. This allows easier automated deployments.
28 - Bumped mercurial to 6.7.4
29 - Mercurial: enable httppostarguments for better support of large repositories with lots of heads.
30 - Added explicit db-migrate step to update hooks for 5.X release.
31
32
33 Security
34 ^^^^^^^^
35
36
37
38 Performance
39 ^^^^^^^^^^^
40
41 - Introduced a full rewrite of ssh backend for performance. The result is 2-5x speed improvement for operation with ssh.
42 enable new ssh wrapper by setting: `ssh.wrapper_cmd = /home/rhodecode/venv/bin/rc-ssh-wrapper-v2`
43 - Introduced a new hooks subsystem that is more scalable and faster, enable it by settings: `vcs.hooks.protocol = celery`
44
45
46 Fixes
47 ^^^^^
48
49 - Archives: Zip archive download breaks when a gitmodules file is present
50 - Branch permissions: fixed bug preventing to specify own rules from 4.X install
51 - SVN: refactored svn events, thus fixing support for it in dockerized env
52 - Fixed empty server url in PR link after push from cli
53
54
55 Upgrade notes
56 ^^^^^^^^^^^^^
57
58 - RhodeCode 5.1.0 is a mayor feature release after big 5.0.0 python3 migration. Happy to ship a first time feature
59 rich release
@@ -0,0 +1,55 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
20 import pytest
21
22 from rhodecode.api.tests.utils import (
23 build_data, api_call)
24
25
26 @pytest.mark.usefixtures("app")
27 class TestServiceApi:
28
29 def test_service_api_with_wrong_secret(self):
30 id, payload = build_data("wrong_api_key", 'service_get_repo_name_by_id')
31 response = api_call(self.app, payload)
32
33 assert 'Invalid API KEY' == response.json['error']
34
35 def test_service_api_with_legit_secret(self):
36 id, payload = build_data(self.app.app.config.get_settings()['app.service_api.token'],
37 'service_get_repo_name_by_id', repo_id='1')
38 response = api_call(self.app, payload)
39 assert not response.json['error']
40
41 def test_service_api_not_a_part_of_public_api_suggestions(self):
42 id, payload = build_data("secret", 'some_random_guess_method')
43 response = api_call(self.app, payload)
44 assert 'service_' not in response.json['error']
45
46 def test_service_get_data_for_ssh_wrapper_output(self):
47 id, payload = build_data(
48 self.app.app.config.get_settings()['app.service_api.token'],
49 'service_get_data_for_ssh_wrapper',
50 user_id=1,
51 repo_name='vcs_test_git')
52 response = api_call(self.app, payload)
53
54 assert ['branch_permissions', 'repo_permissions', 'repos_path', 'user_id', 'username']\
55 == list(response.json['result'].keys())
@@ -0,0 +1,125 b''
1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import logging
20 import datetime
21 from collections import defaultdict
22
23 from sqlalchemy import Table
24 from rhodecode.api import jsonrpc_method, SERVICE_API_IDENTIFIER
25
26
27 log = logging.getLogger(__name__)
28
29
30 @jsonrpc_method()
31 def service_get_data_for_ssh_wrapper(request, apiuser, user_id, repo_name, key_id=None):
32 from rhodecode.model.db import User
33 from rhodecode.model.scm import ScmModel
34 from rhodecode.model.meta import raw_query_executor, Base
35
36 if key_id:
37 table = Table('user_ssh_keys', Base.metadata, autoload=False)
38 atime = datetime.datetime.utcnow()
39 stmt = (
40 table.update()
41 .where(table.c.ssh_key_id == key_id)
42 .values(accessed_on=atime)
43 )
44
45 res_count = None
46 with raw_query_executor() as session:
47 result = session.execute(stmt)
48 if result.rowcount:
49 res_count = result.rowcount
50
51 if res_count:
52 log.debug(f'Update key id:{key_id} access time')
53 db_user = User.get(user_id)
54 if not db_user:
55 return None
56 auth_user = db_user.AuthUser()
57
58 return {
59 'user_id': db_user.user_id,
60 'username': db_user.username,
61 'repo_permissions': auth_user.permissions['repositories'],
62 "branch_permissions": auth_user.get_branch_permissions(repo_name),
63 "repos_path": ScmModel().repos_path
64 }
65
66
67 @jsonrpc_method()
68 def service_get_repo_name_by_id(request, apiuser, repo_id):
69 from rhodecode.model.repo import RepoModel
70 by_id_match = RepoModel().get_repo_by_id(repo_id)
71 if by_id_match:
72 repo_name = by_id_match.repo_name
73 return {
74 'repo_name': repo_name
75 }
76 return None
77
78
79 @jsonrpc_method()
80 def service_mark_for_invalidation(request, apiuser, repo_name):
81 from rhodecode.model.scm import ScmModel
82 ScmModel().mark_for_invalidation(repo_name)
83 return {'msg': "Applied"}
84
85
86 @jsonrpc_method()
87 def service_config_to_hgrc(request, apiuser, cli_flags, repo_name):
88 from rhodecode.model.db import RhodeCodeUi
89 from rhodecode.model.settings import VcsSettingsModel
90
91 ui_sections = defaultdict(list)
92 ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None)
93
94 default_hooks = [
95 ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'),
96 ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'),
97 ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'),
98
99 ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'),
100 ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'),
101 ]
102
103 for k, v in default_hooks:
104 ui_sections['hooks'].append((k, v))
105
106 for entry in ui:
107 if not entry.active:
108 continue
109 sec = entry.section
110 key = entry.key
111
112 if sec in cli_flags:
113 # we want only custom hooks, so we skip builtins
114 if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN:
115 continue
116
117 ui_sections[sec].append([key, entry.value])
118
119 flags = []
120 for _sec, key_val in ui_sections.items():
121 flags.append(' ')
122 flags.append(f'[{_sec}]')
123 for key, val in key_val:
124 flags.append(f'{key}= {val}')
125 return {'flags': flags}
@@ -0,0 +1,67 b''
1 import pytest
2 import mock
3
4 from rhodecode.lib.type_utils import AttributeDict
5 from rhodecode.model.meta import Session
6 from rhodecode.tests.fixture import Fixture
7 from rhodecode.tests.routes import route_path
8 from rhodecode.model.settings import SettingsModel
9
10 fixture = Fixture()
11
12
13 @pytest.mark.usefixtures('app')
14 class Test2FA(object):
15 @classmethod
16 def setup_class(cls):
17 cls.password = 'valid-one'
18
19 def test_redirect_to_2fa_setup_if_enabled_for_user(self, user_util):
20 user = user_util.create_user(password=self.password)
21 user.has_enabled_2fa = True
22 self.app.post(
23 route_path('login'),
24 {'username': user.username,
25 'password': self.password})
26
27 response = self.app.get('/')
28 assert response.status_code == 302
29 assert response.location.endswith(route_path('setup_2fa'))
30
31 def test_redirect_to_2fa_check_if_2fa_configured(self, user_util):
32 user = user_util.create_user(password=self.password)
33 user.has_enabled_2fa = True
34 user.init_secret_2fa()
35 Session().add(user)
36 Session().commit()
37 self.app.post(
38 route_path('login'),
39 {'username': user.username,
40 'password': self.password})
41 response = self.app.get('/')
42 assert response.status_code == 302
43 assert response.location.endswith(route_path('check_2fa'))
44
45 def test_2fa_recovery_codes_works_only_once(self, user_util):
46 user = user_util.create_user(password=self.password)
47 user.has_enabled_2fa = True
48 user.init_secret_2fa()
49 recovery_code_to_check = user.init_2fa_recovery_codes()[0]
50 Session().add(user)
51 Session().commit()
52 self.app.post(
53 route_path('login'),
54 {'username': user.username,
55 'password': self.password})
56 response = self.app.post(route_path('check_2fa'), {'totp': recovery_code_to_check})
57 assert response.status_code == 302
58 response = self.app.post(route_path('check_2fa'), {'totp': recovery_code_to_check})
59 response.mustcontain('Code is invalid. Try again!')
60
61 def test_2fa_state_when_forced_by_admin(self, user_util):
62 user = user_util.create_user(password=self.password)
63 user.has_enabled_2fa = False
64 with mock.patch.object(
65 SettingsModel, 'get_setting_by_name', lambda *a, **kw: AttributeDict(app_settings_value=True)):
66
67 assert user.has_enabled_2fa
@@ -0,0 +1,98 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 """
20 WARNING: be really carefully with changing ANY imports in this file
21 # This script is to mean as really fast executable, doing some imports here that would yield an import chain change
22 # can affect execution times...
23 # This can be easily debugged using such command::
24 # time PYTHONPROFILEIMPORTTIME=1 rc-ssh-wrapper-v2 --debug --mode=test .dev/dev.ini
25 """
26
27 import os
28 import sys
29 import time
30 import logging
31
32 import click
33
34 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
35 from rhodecode.lib.request import Request
36 from rhodecode.lib.utils2 import AttributeDict
37 from rhodecode.lib.statsd_client import StatsdClient
38 from rhodecode.lib.config_utils import get_app_config_lightweight
39
40 from .utils import setup_custom_logging
41 from .backends import SshWrapperStandalone
42
43 log = logging.getLogger(__name__)
44
45
46 @click.command()
47 @click.argument('ini_path', type=click.Path(exists=True))
48 @click.option(
49 '--mode', '-m', required=False, default='auto',
50 type=click.Choice(['auto', 'vcs', 'git', 'hg', 'svn', 'test']),
51 help='mode of operation')
52 @click.option('--user', help='Username for which the command will be executed')
53 @click.option('--user-id', help='User ID for which the command will be executed')
54 @click.option('--key-id', help='ID of the key from the database')
55 @click.option('--shell', '-s', is_flag=True, help='Allow Shell')
56 @click.option('--debug', is_flag=True, help='Enabled detailed output logging')
57 def main(ini_path, mode, user, user_id, key_id, shell, debug):
58
59 time_start = time.time()
60 setup_custom_logging(ini_path, debug)
61
62 command = os.environ.get('SSH_ORIGINAL_COMMAND', '')
63 if not command and mode not in ['test']:
64 raise ValueError(
65 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.'
66 'Please make sure this is set and available during execution '
67 'of this script.')
68
69 # initialize settings and get defaults
70 settings = get_app_config_lightweight(ini_path)
71 settings = sanitize_settings_and_apply_defaults({'__file__': ini_path}, settings)
72
73 # init and bootstrap StatsdClient
74 StatsdClient.setup(settings)
75 statsd = StatsdClient.statsd
76
77 try:
78 connection_info = os.environ.get('SSH_CONNECTION', '')
79 request = Request.blank('/', base_url=settings['app.base_url'])
80 request.user = AttributeDict({'username': user,
81 'user_id': user_id,
82 'ip_addr': connection_info.split(' ')[0] if connection_info else None})
83 env = {'RC_CMD_SSH_WRAPPER': '1', 'request': request}
84 ssh_wrapper = SshWrapperStandalone(
85 command, connection_info, mode,
86 user, user_id, key_id, shell, ini_path, settings, env)
87 except Exception:
88 log.exception('Failed to execute SshWrapper')
89 sys.exit(-5)
90
91 return_code = ssh_wrapper.wrap()
92 operation_took = time.time() - time_start
93 if statsd:
94 operation_took_ms = round(1000.0 * operation_took)
95 statsd.timing("rhodecode_ssh_wrapper_timing.histogram", operation_took_ms,
96 use_decimals=False)
97
98 sys.exit(return_code)
@@ -0,0 +1,34 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import logging
20
21
22 def setup_custom_logging(ini_path, debug):
23 if debug:
24 from pyramid.paster import setup_logging # Lazy import
25 # enabled rhodecode.ini controlled logging setup
26 setup_logging(ini_path)
27 else:
28 # configure logging in a mode that doesn't print anything.
29 # in case of regularly configured logging it gets printed out back
30 # to the client doing an SSH command.
31 logger = logging.getLogger('')
32 null = logging.NullHandler()
33 # add the handler to the root logger
34 logger.handlers = [null]
@@ -0,0 +1,224 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import tempfile
21 import logging
22
23 from pyramid.settings import asbool
24
25 from rhodecode.config.settings_maker import SettingsMaker
26 from rhodecode.config import utils as config_utils
27
28 log = logging.getLogger(__name__)
29
30
31 def sanitize_settings_and_apply_defaults(global_config, settings):
32 """
33 Applies settings defaults and does all type conversion.
34
35 We would move all settings parsing and preparation into this place, so that
36 we have only one place left which deals with this part. The remaining parts
37 of the application would start to rely fully on well-prepared settings.
38
39 This piece would later be split up per topic to avoid a big fat monster
40 function.
41 """
42 jn = os.path.join
43
44 global_settings_maker = SettingsMaker(global_config)
45 global_settings_maker.make_setting('debug', default=False, parser='bool')
46 debug_enabled = asbool(global_config.get('debug'))
47
48 settings_maker = SettingsMaker(settings)
49
50 settings_maker.make_setting(
51 'logging.autoconfigure',
52 default=False,
53 parser='bool')
54
55 logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini')
56 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
57
58 # Default includes, possible to change as a user
59 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
60 log.debug(
61 "Using the following pyramid.includes: %s",
62 pyramid_includes)
63
64 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
65 settings_maker.make_setting('rhodecode.edition_id', 'CE')
66
67 if 'mako.default_filters' not in settings:
68 # set custom default filters if we don't have it defined
69 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
70 settings['mako.default_filters'] = 'h_filter'
71
72 if 'mako.directories' not in settings:
73 mako_directories = settings.setdefault('mako.directories', [
74 # Base templates of the original application
75 'rhodecode:templates',
76 ])
77 log.debug(
78 "Using the following Mako template directories: %s",
79 mako_directories)
80
81 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
82 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
83 raw_url = settings['beaker.session.url']
84 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
85 settings['beaker.session.url'] = 'redis://' + raw_url
86
87 settings_maker.make_setting('__file__', global_config.get('__file__'))
88
89 # TODO: johbo: Re-think this, usually the call to config.include
90 # should allow to pass in a prefix.
91 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
92
93 # Sanitize generic settings.
94 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
95 settings_maker.make_setting('gzip_responses', False, parser='bool')
96 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
97
98 # statsd
99 settings_maker.make_setting('statsd.enabled', False, parser='bool')
100 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
101 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
102 settings_maker.make_setting('statsd.statsd_prefix', '')
103 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
104
105 settings_maker.make_setting('vcs.svn.compatible_version', '')
106 settings_maker.make_setting('vcs.svn.redis_conn', 'redis://redis:6379/0')
107 settings_maker.make_setting('vcs.svn.proxy.enabled', True, parser='bool')
108 settings_maker.make_setting('vcs.svn.proxy.host', 'http://svn:8090', parser='string')
109 settings_maker.make_setting('vcs.hooks.protocol', 'http')
110 settings_maker.make_setting('vcs.hooks.host', '*')
111 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
112 settings_maker.make_setting('vcs.server', '')
113 settings_maker.make_setting('vcs.server.protocol', 'http')
114 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
115 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
116 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
117 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
118 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
119
120 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
121
122 # repo_store path
123 settings_maker.make_setting('repo_store.path', '/var/opt/rhodecode_repo_store')
124 # Support legacy values of vcs.scm_app_implementation. Legacy
125 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
126 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
127 scm_app_impl = settings['vcs.scm_app_implementation']
128 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
129 settings['vcs.scm_app_implementation'] = 'http'
130
131 settings_maker.make_setting('appenlight', False, parser='bool')
132
133 temp_store = tempfile.gettempdir()
134 tmp_cache_dir = jn(temp_store, 'rc_cache')
135
136 # save default, cache dir, and use it for all backends later.
137 default_cache_dir = settings_maker.make_setting(
138 'cache_dir',
139 default=tmp_cache_dir, default_when_empty=True,
140 parser='dir:ensured')
141
142 # exception store cache
143 settings_maker.make_setting(
144 'exception_tracker.store_path',
145 default=jn(default_cache_dir, 'exc_store'), default_when_empty=True,
146 parser='dir:ensured'
147 )
148
149 settings_maker.make_setting(
150 'celerybeat-schedule.path',
151 default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
152 parser='file:ensured'
153 )
154
155 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
156 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
157
158 # sessions, ensure file since no-value is memory
159 settings_maker.make_setting('beaker.session.type', 'file')
160 settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data'))
161
162 # cache_general
163 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
164 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
165 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db'))
166
167 # cache_perms
168 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
169 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
170 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db'))
171
172 # cache_repo
173 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
174 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
175 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db'))
176
177 # cache_license
178 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
179 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
180 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db'))
181
182 # cache_repo_longterm memory, 96H
183 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
184 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
185 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
186
187 # sql_cache_short
188 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
189 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
190 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
191
192 # archive_cache
193 settings_maker.make_setting('archive_cache.locking.url', 'redis://redis:6379/1')
194 settings_maker.make_setting('archive_cache.backend.type', 'filesystem')
195
196 settings_maker.make_setting('archive_cache.filesystem.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
197 settings_maker.make_setting('archive_cache.filesystem.cache_shards', 8, parser='int')
198 settings_maker.make_setting('archive_cache.filesystem.cache_size_gb', 10, parser='float')
199 settings_maker.make_setting('archive_cache.filesystem.eviction_policy', 'least-recently-stored')
200
201 settings_maker.make_setting('archive_cache.filesystem.retry', False, parser='bool')
202 settings_maker.make_setting('archive_cache.filesystem.retry_backoff', 1, parser='int')
203 settings_maker.make_setting('archive_cache.filesystem.retry_attempts', 10, parser='int')
204
205 settings_maker.make_setting('archive_cache.objectstore.url', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
206 settings_maker.make_setting('archive_cache.objectstore.key', '')
207 settings_maker.make_setting('archive_cache.objectstore.secret', '')
208 settings_maker.make_setting('archive_cache.objectstore.region', 'eu-central-1')
209 settings_maker.make_setting('archive_cache.objectstore.bucket', 'rhodecode-archive-cache', default_when_empty=True,)
210 settings_maker.make_setting('archive_cache.objectstore.bucket_shards', 8, parser='int')
211
212 settings_maker.make_setting('archive_cache.objectstore.cache_size_gb', 10, parser='float')
213 settings_maker.make_setting('archive_cache.objectstore.eviction_policy', 'least-recently-stored')
214
215 settings_maker.make_setting('archive_cache.objectstore.retry', False, parser='bool')
216 settings_maker.make_setting('archive_cache.objectstore.retry_backoff', 1, parser='int')
217 settings_maker.make_setting('archive_cache.objectstore.retry_attempts', 10, parser='int')
218
219 settings_maker.env_expand()
220
221 # configure instance id
222 config_utils.set_instance_id(settings)
223
224 return settings
@@ -0,0 +1,47 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import urllib.parse
20
21 from rhodecode.lib.vcs import CurlSession
22 from rhodecode.lib.ext_json import json
23 from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError
24
25
26 def call_service_api(settings, payload):
27 try:
28 api_host = settings['app.service_api.host']
29 api_token = settings['app.service_api.token']
30 api_url = settings['rhodecode.api.url']
31 except KeyError as exc:
32 raise ImproperlyConfiguredError(
33 f"{str(exc)} is missing. "
34 "Please ensure that app.service_api.host, app.service_api.token and rhodecode.api.url are "
35 "defined inside of .ini configuration file."
36 )
37 payload.update({
38 'id': 'service',
39 'auth_token': api_token
40 })
41 service_api_url = urllib.parse.urljoin(api_host, api_url)
42 response = CurlSession().post(service_api_url, json.dumps(payload))
43
44 if response.status_code != 200:
45 raise Exception(f"Service API at {service_api_url} responded with error: {response.status_code}")
46
47 return json.loads(response.content)['result']
@@ -0,0 +1,78 b''
1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import logging
20
21 from .backends.fanout_cache import FileSystemFanoutCache
22 from .backends.objectstore_cache import ObjectStoreCache
23
24 from .utils import archive_iterator # noqa
25 from .lock import ArchiveCacheGenerationLock # noqa
26
27 log = logging.getLogger(__name__)
28
29
30 cache_meta = None
31
32
33 def includeme(config):
34 # init our cache at start
35 settings = config.get_settings()
36 get_archival_cache_store(settings)
37
38
39 def get_archival_config(config):
40
41 final_config = {
42
43 }
44
45 for k, v in config.items():
46 if k.startswith('archive_cache'):
47 final_config[k] = v
48
49 return final_config
50
51
52 def get_archival_cache_store(config, always_init=False):
53
54 global cache_meta
55 if cache_meta is not None and not always_init:
56 return cache_meta
57
58 config = get_archival_config(config)
59 backend = config['archive_cache.backend.type']
60
61 archive_cache_locking_url = config['archive_cache.locking.url']
62
63 match backend:
64 case 'filesystem':
65 d_cache = FileSystemFanoutCache(
66 locking_url=archive_cache_locking_url,
67 **config
68 )
69 case 'objectstore':
70 d_cache = ObjectStoreCache(
71 locking_url=archive_cache_locking_url,
72 **config
73 )
74 case _:
75 raise ValueError(f'archive_cache.backend.type only supports "filesystem" or "objectstore" got {backend} ')
76
77 cache_meta = d_cache
78 return cache_meta
@@ -0,0 +1,17 b''
1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,372 b''
1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import functools
21 import logging
22 import typing
23 import time
24 import zlib
25
26 from ...ext_json import json
27 from ..utils import StatsDB, NOT_GIVEN, ShardFileReader, EVICTION_POLICY, format_size
28 from ..lock import GenerationLock
29
30 log = logging.getLogger(__name__)
31
32
33 class BaseShard:
34 storage_type: str = ''
35 fs = None
36
37 @classmethod
38 def hash(cls, key):
39 """Compute portable hash for `key`.
40
41 :param key: key to hash
42 :return: hash value
43
44 """
45 mask = 0xFFFFFFFF
46 return zlib.adler32(key.encode('utf-8')) & mask # noqa
47
48 def _write_file(self, full_path, read_iterator, mode):
49 raise NotImplementedError
50
51 def _get_keyfile(self, key):
52 raise NotImplementedError
53
54 def random_filename(self):
55 raise NotImplementedError
56
57 def store(self, *args, **kwargs):
58 raise NotImplementedError
59
60 def _store(self, key, value_reader, metadata, mode):
61 (filename, # hash-name
62 full_path # full-path/hash-name
63 ) = self.random_filename()
64
65 key_file, key_file_path = self._get_keyfile(key)
66
67 # STORE METADATA
68 _metadata = {
69 "version": "v1",
70
71 "key_file": key_file, # this is the .key.json file storing meta
72 "key_file_path": key_file_path, # full path to key_file
73 "archive_key": key, # original name we stored archive under, e.g my-archive.zip
74 "archive_filename": filename, # the actual filename we stored that file under
75 "archive_full_path": full_path,
76
77 "store_time": time.time(),
78 "access_count": 0,
79 "access_time": 0,
80
81 "size": 0
82 }
83 if metadata:
84 _metadata.update(metadata)
85
86 read_iterator = iter(functools.partial(value_reader.read, 2**22), b'')
87 size, sha256 = self._write_file(full_path, read_iterator, mode)
88 _metadata['size'] = size
89 _metadata['sha256'] = sha256
90
91 # after archive is finished, we create a key to save the presence of the binary file
92 with self.fs.open(key_file_path, 'wb') as f:
93 f.write(json.dumps(_metadata))
94
95 return key, filename, size, _metadata
96
97 def fetch(self, *args, **kwargs):
98 raise NotImplementedError
99
100 def _fetch(self, key, retry, retry_attempts, retry_backoff,
101 presigned_url_expires: int = 0) -> tuple[ShardFileReader, dict]:
102 if retry is NOT_GIVEN:
103 retry = False
104 if retry_attempts is NOT_GIVEN:
105 retry_attempts = 0
106
107 if retry and retry_attempts > 0:
108 for attempt in range(1, retry_attempts + 1):
109 if key in self:
110 break
111 # we didn't find the key, wait retry_backoff N seconds, and re-check
112 time.sleep(retry_backoff)
113
114 if key not in self:
115 log.exception(f'requested key={key} not found in {self} retry={retry}, attempts={retry_attempts}')
116 raise KeyError(key)
117
118 key_file, key_file_path = self._get_keyfile(key)
119 with self.fs.open(key_file_path, 'rb') as f:
120 metadata = json.loads(f.read())
121
122 archive_path = metadata['archive_full_path']
123 if presigned_url_expires and presigned_url_expires > 0:
124 metadata['url'] = self.fs.url(archive_path, expires=presigned_url_expires)
125
126 try:
127 return ShardFileReader(self.fs.open(archive_path, 'rb')), metadata
128 finally:
129 # update usage stats, count and accessed
130 metadata["access_count"] = metadata.get("access_count", 0) + 1
131 metadata["access_time"] = time.time()
132 log.debug('Updated %s with access snapshot, access_count=%s access_time=%s',
133 key_file, metadata['access_count'], metadata['access_time'])
134 with self.fs.open(key_file_path, 'wb') as f:
135 f.write(json.dumps(metadata))
136
137 def remove(self, *args, **kwargs):
138 raise NotImplementedError
139
140 def _remove(self, key):
141 if key not in self:
142 log.exception(f'requested key={key} not found in {self}')
143 raise KeyError(key)
144
145 key_file, key_file_path = self._get_keyfile(key)
146 with self.fs.open(key_file_path, 'rb') as f:
147 metadata = json.loads(f.read())
148
149 archive_path = metadata['archive_full_path']
150 self.fs.rm(archive_path)
151 self.fs.rm(key_file_path)
152 return 1
153
154 @property
155 def storage_medium(self):
156 return getattr(self, self.storage_type)
157
158 @property
159 def key_suffix(self):
160 return 'key.json'
161
162 def __contains__(self, key):
163 """Return `True` if `key` matching item is found in cache.
164
165 :param key: key matching item
166 :return: True if key matching item
167
168 """
169 key_file, key_file_path = self._get_keyfile(key)
170 return self.fs.exists(key_file_path)
171
172
173 class BaseCache:
174 _locking_url: str = ''
175 _storage_path: str = ''
176 _config: dict = {}
177 retry = False
178 retry_attempts: int = 0
179 retry_backoff: int | float = 1
180 _shards = tuple()
181 shard_cls = BaseShard
182 # define the presigned url expiration, 0 == disabled
183 presigned_url_expires: int = 0
184
185 def __contains__(self, key):
186 """Return `True` if `key` matching item is found in cache.
187
188 :param key: key matching item
189 :return: True if key matching item
190
191 """
192 return self.has_key(key)
193
194 def __repr__(self):
195 return f'<{self.__class__.__name__}(storage={self._storage_path})>'
196
197 @classmethod
198 def gb_to_bytes(cls, gb):
199 return gb * (1024 ** 3)
200
201 @property
202 def storage_path(self):
203 return self._storage_path
204
205 @classmethod
206 def get_stats_db(cls):
207 return StatsDB()
208
209 def get_conf(self, key, pop=False):
210 if key not in self._config:
211 raise ValueError(f"No configuration key '{key}', please make sure it exists in archive_cache config")
212 val = self._config[key]
213 if pop:
214 del self._config[key]
215 return val
216
217 def _get_shard(self, key) -> shard_cls:
218 index = self._hash(key) % self._shard_count
219 shard = self._shards[index]
220 return shard
221
222 def _get_size(self, shard, archive_path):
223 raise NotImplementedError
224
225 def store(self, key, value_reader, metadata=None):
226 shard = self._get_shard(key)
227 return shard.store(key, value_reader, metadata)
228
229 def fetch(self, key, retry=NOT_GIVEN, retry_attempts=NOT_GIVEN) -> tuple[typing.BinaryIO, dict]:
230 """
231 Return file handle corresponding to `key` from specific shard cache.
232 """
233 if retry is NOT_GIVEN:
234 retry = self.retry
235 if retry_attempts is NOT_GIVEN:
236 retry_attempts = self.retry_attempts
237 retry_backoff = self.retry_backoff
238 presigned_url_expires = self.presigned_url_expires
239
240 shard = self._get_shard(key)
241 return shard.fetch(key, retry=retry,
242 retry_attempts=retry_attempts,
243 retry_backoff=retry_backoff,
244 presigned_url_expires=presigned_url_expires)
245
246 def remove(self, key):
247 shard = self._get_shard(key)
248 return shard.remove(key)
249
250 def has_key(self, archive_key):
251 """Return `True` if `key` matching item is found in cache.
252
253 :param archive_key: key for item, this is a unique archive name we want to store data under. e.g my-archive-svn.zip
254 :return: True if key is found
255
256 """
257 shard = self._get_shard(archive_key)
258 return archive_key in shard
259
260 def iter_keys(self):
261 for shard in self._shards:
262 if shard.fs.exists(shard.storage_medium):
263 for path, _dirs, _files in shard.fs.walk(shard.storage_medium):
264 for key_file_path in _files:
265 if key_file_path.endswith(shard.key_suffix):
266 yield shard, key_file_path
267
268 def get_lock(self, lock_key):
269 return GenerationLock(lock_key, self._locking_url)
270
271 def evict(self, policy=None, size_limit=None) -> dict:
272 """
273 Remove old items based on the conditions
274
275
276 explanation of this algo:
277 iterate over each shard, then for each shard iterate over the .key files
278 read the key files metadata stored. This gives us a full list of keys, cached_archived, their size and
279 access data, time creation, and access counts.
280
281 Store that into a memory DB in order we can run different sorting strategies easily.
282 Summing the size is a sum sql query.
283
284 Then we run a sorting strategy based on eviction policy.
285 We iterate over sorted keys, and remove each checking if we hit the overall limit.
286 """
287 removal_info = {
288 "removed_items": 0,
289 "removed_size": 0
290 }
291 policy = policy or self._eviction_policy
292 size_limit = size_limit or self._cache_size_limit
293
294 select_policy = EVICTION_POLICY[policy]['evict']
295
296 log.debug('Running eviction policy \'%s\', and checking for size limit: %s',
297 policy, format_size(size_limit))
298
299 if select_policy is None:
300 return removal_info
301
302 db = self.get_stats_db()
303
304 data = []
305 cnt = 1
306
307 for shard, key_file in self.iter_keys():
308 with shard.fs.open(os.path.join(shard.storage_medium, key_file), 'rb') as f:
309 metadata = json.loads(f.read())
310
311 key_file_path = os.path.join(shard.storage_medium, key_file)
312
313 archive_key = metadata['archive_key']
314 archive_path = metadata['archive_full_path']
315
316 size = metadata.get('size')
317 if not size:
318 # in case we don't have size re-calc it...
319 size = self._get_size(shard, archive_path)
320
321 data.append([
322 cnt,
323 key_file,
324 key_file_path,
325 archive_key,
326 archive_path,
327 metadata.get('store_time', 0),
328 metadata.get('access_time', 0),
329 metadata.get('access_count', 0),
330 size,
331 ])
332 cnt += 1
333
334 # Insert bulk data using executemany
335 db.bulk_insert(data)
336
337 total_size = db.get_total_size()
338 log.debug('Analyzed %s keys, occupying: %s, running eviction to match %s',
339 len(data), format_size(total_size), format_size(size_limit))
340
341 removed_items = 0
342 removed_size = 0
343 for key_file, archive_key, size in db.get_sorted_keys(select_policy):
344 # simulate removal impact BEFORE removal
345 total_size -= size
346
347 if total_size <= size_limit:
348 # we obtained what we wanted...
349 break
350
351 self.remove(archive_key)
352 removed_items += 1
353 removed_size += size
354 removal_info['removed_items'] = removed_items
355 removal_info['removed_size'] = removed_size
356 log.debug('Removed %s cache archives, and reduced size by: %s',
357 removed_items, format_size(removed_size))
358 return removal_info
359
360 def get_statistics(self):
361 total_files = 0
362 total_size = 0
363 meta = {}
364
365 for shard, key_file in self.iter_keys():
366 json_key = f"{shard.storage_medium}/{key_file}"
367 with shard.fs.open(json_key, 'rb') as f:
368 total_files += 1
369 metadata = json.loads(f.read())
370 total_size += metadata['size']
371
372 return total_files, total_size, meta
@@ -0,0 +1,173 b''
1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import codecs
20 import hashlib
21 import logging
22 import os
23 import typing
24
25 import fsspec
26
27 from .base import BaseCache, BaseShard
28 from ..utils import ShardFileReader, NOT_GIVEN
29 from ...type_utils import str2bool
30
31 log = logging.getLogger(__name__)
32
33
34 class S3Shard(BaseShard):
35
36 def __init__(self, index, bucket, bucket_folder, fs, **settings):
37 self._index: int = index
38 self._bucket_folder: str = bucket_folder
39 self.storage_type: str = 'bucket'
40 self._bucket_main: str = bucket
41
42 self.fs = fs
43
44 @property
45 def bucket(self) -> str:
46 """Cache bucket final path."""
47 return os.path.join(self._bucket_main, self._bucket_folder)
48
49 def _get_keyfile(self, archive_key) -> tuple[str, str]:
50 key_file: str = f'{archive_key}-{self.key_suffix}'
51 return key_file, os.path.join(self.bucket, key_file)
52
53 def _get_writer(self, path, mode):
54 return self.fs.open(path, 'wb')
55
56 def _write_file(self, full_path, iterator, mode):
57
58 # ensure folder in bucket exists
59 destination = self.bucket
60 if not self.fs.exists(destination):
61 self.fs.mkdir(destination, s3_additional_kwargs={})
62
63 writer = self._get_writer(full_path, mode)
64
65 digest = hashlib.sha256()
66 with writer:
67 size = 0
68 for chunk in iterator:
69 size += len(chunk)
70 digest.update(chunk)
71 writer.write(chunk)
72
73 sha256 = digest.hexdigest()
74 log.debug('written new archive cache under %s, sha256: %s', full_path, sha256)
75 return size, sha256
76
77 def store(self, key, value_reader, metadata: dict | None = None):
78 return self._store(key, value_reader, metadata, mode='wb')
79
80 def fetch(self, key, retry=NOT_GIVEN,
81 retry_attempts=NOT_GIVEN, retry_backoff=1,
82 presigned_url_expires: int = 0) -> tuple[ShardFileReader, dict]:
83 return self._fetch(key, retry, retry_attempts, retry_backoff, presigned_url_expires=presigned_url_expires)
84
85 def remove(self, key):
86 return self._remove(key)
87
88 def random_filename(self):
89 """Return filename and full-path tuple for file storage.
90
91 Filename will be a randomly generated 28 character hexadecimal string
92 with ".archive_cache" suffixed. Two levels of sub-directories will be used to
93 reduce the size of directories. On older filesystems, lookups in
94 directories with many files may be slow.
95 """
96
97 hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8')
98
99 archive_name = hex_name[4:] + '.archive_cache'
100 filename = f"{hex_name[:2]}-{hex_name[2:4]}-{archive_name}"
101
102 full_path = os.path.join(self.bucket, filename)
103 return archive_name, full_path
104
105 def __repr__(self):
106 return f'{self.__class__.__name__}(index={self._index}, bucket={self.bucket})'
107
108
109 class ObjectStoreCache(BaseCache):
110 shard_name: str = 'shard-{:03d}'
111 shard_cls = S3Shard
112
113 def __init__(self, locking_url, **settings):
114 """
115 Initialize objectstore cache instance.
116
117 :param str locking_url: redis url for a lock
118 :param settings: settings dict
119
120 """
121 self._locking_url = locking_url
122 self._config = settings
123
124 objectstore_url = self.get_conf('archive_cache.objectstore.url')
125 self._storage_path = objectstore_url # common path for all from BaseCache
126
127 self._shard_count = int(self.get_conf('archive_cache.objectstore.bucket_shards', pop=True))
128 if self._shard_count < 1:
129 raise ValueError('cache_shards must be 1 or more')
130
131 self._bucket = settings.pop('archive_cache.objectstore.bucket')
132 if not self._bucket:
133 raise ValueError('archive_cache.objectstore.bucket needs to have a value')
134
135 self._eviction_policy = self.get_conf('archive_cache.objectstore.eviction_policy', pop=True)
136 self._cache_size_limit = self.gb_to_bytes(int(self.get_conf('archive_cache.objectstore.cache_size_gb')))
137
138 self.retry = str2bool(self.get_conf('archive_cache.objectstore.retry', pop=True))
139 self.retry_attempts = int(self.get_conf('archive_cache.objectstore.retry_attempts', pop=True))
140 self.retry_backoff = int(self.get_conf('archive_cache.objectstore.retry_backoff', pop=True))
141
142 endpoint_url = settings.pop('archive_cache.objectstore.url')
143 key = settings.pop('archive_cache.objectstore.key')
144 secret = settings.pop('archive_cache.objectstore.secret')
145 region = settings.pop('archive_cache.objectstore.region')
146
147 log.debug('Initializing %s archival cache instance', self)
148
149 fs = fsspec.filesystem(
150 's3', anon=False, endpoint_url=endpoint_url, key=key, secret=secret, client_kwargs={'region_name': region}
151 )
152
153 # init main bucket
154 if not fs.exists(self._bucket):
155 fs.mkdir(self._bucket)
156
157 self._shards = tuple(
158 self.shard_cls(
159 index=num,
160 bucket=self._bucket,
161 bucket_folder=self.shard_name.format(num),
162 fs=fs,
163 **settings,
164 )
165 for num in range(self._shard_count)
166 )
167 self._hash = self._shards[0].hash
168
169 def _get_size(self, shard, archive_path):
170 return shard.fs.info(archive_path)['size']
171
172 def set_presigned_url_expiry(self, val: int) -> None:
173 self.presigned_url_expires = val
@@ -0,0 +1,62 b''
1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import redis
20 from .._vendor import redis_lock
21
22
23 class ArchiveCacheGenerationLock(Exception):
24 pass
25
26
27 class GenerationLock:
28 """
29 Locking mechanism that detects if a lock is acquired
30
31 with GenerationLock(lock_key):
32 compute_archive()
33 """
34 lock_timeout = 7200
35
36 def __init__(self, lock_key, url):
37 self.lock_key = lock_key
38 self._create_client(url)
39 self.lock = self.get_lock()
40
41 def _create_client(self, url):
42 connection_pool = redis.ConnectionPool.from_url(url)
43 self.writer_client = redis.StrictRedis(
44 connection_pool=connection_pool
45 )
46 self.reader_client = self.writer_client
47
48 def get_lock(self):
49 return redis_lock.Lock(
50 redis_client=self.writer_client,
51 name=self.lock_key,
52 expire=self.lock_timeout,
53 strict=True
54 )
55
56 def __enter__(self):
57 acquired = self.lock.acquire(blocking=False)
58 if not acquired:
59 raise ArchiveCacheGenerationLock('Failed to create a lock')
60
61 def __exit__(self, exc_type, exc_val, exc_tb):
62 self.lock.release()
@@ -0,0 +1,134 b''
1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import sqlite3
20 import s3fs.core
21
22 NOT_GIVEN = -917
23
24
25 EVICTION_POLICY = {
26 'none': {
27 'evict': None,
28 },
29 'least-recently-stored': {
30 'evict': 'SELECT {fields} FROM archive_cache ORDER BY store_time',
31 },
32 'least-recently-used': {
33 'evict': 'SELECT {fields} FROM archive_cache ORDER BY access_time',
34 },
35 'least-frequently-used': {
36 'evict': 'SELECT {fields} FROM archive_cache ORDER BY access_count',
37 },
38 }
39
40
41 def archive_iterator(_reader, block_size: int = 4096 * 512):
42 # 4096 * 64 = 64KB
43 while 1:
44 data = _reader.read(block_size)
45 if not data:
46 break
47 yield data
48
49
50 def format_size(size):
51 # Convert size in bytes to a human-readable format (e.g., KB, MB, GB)
52 for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
53 if size < 1024:
54 return f"{size:.2f} {unit}"
55 size /= 1024
56
57
58 class StatsDB:
59
60 def __init__(self):
61 self.connection = sqlite3.connect(':memory:')
62 self._init_db()
63
64 def _init_db(self):
65 qry = '''
66 CREATE TABLE IF NOT EXISTS archive_cache (
67 rowid INTEGER PRIMARY KEY,
68 key_file TEXT,
69 key_file_path TEXT,
70 archive_key TEXT,
71 archive_path TEXT,
72 store_time REAL,
73 access_time REAL,
74 access_count INTEGER DEFAULT 0,
75 size INTEGER DEFAULT 0
76 )
77 '''
78
79 self.sql(qry)
80 self.connection.commit()
81
82 @property
83 def sql(self):
84 return self.connection.execute
85
86 def bulk_insert(self, rows):
87 qry = '''
88 INSERT INTO archive_cache (
89 rowid,
90 key_file,
91 key_file_path,
92 archive_key,
93 archive_path,
94 store_time,
95 access_time,
96 access_count,
97 size
98 )
99 VALUES (
100 ?, ?, ?, ?, ?, ?, ?, ?, ?
101 )
102 '''
103 cursor = self.connection.cursor()
104 cursor.executemany(qry, rows)
105 self.connection.commit()
106
107 def get_total_size(self):
108 qry = 'SELECT COALESCE(SUM(size), 0) FROM archive_cache'
109 ((total_size,),) = self.sql(qry).fetchall()
110 return total_size
111
112 def get_sorted_keys(self, select_policy):
113 select_policy_qry = select_policy.format(fields='key_file, archive_key, size')
114 return self.sql(select_policy_qry).fetchall()
115
116
117 class ShardFileReader:
118
119 def __init__(self, file_like_reader):
120 self._file_like_reader = file_like_reader
121
122 def __getattr__(self, item):
123 if isinstance(self._file_like_reader, s3fs.core.S3File):
124 match item:
125 case 'name':
126 # S3 FileWrapper doesn't support name attribute, and we use it
127 return self._file_like_reader.full_name
128 case _:
129 return getattr(self._file_like_reader, item)
130 else:
131 return getattr(self._file_like_reader, item)
132
133 def __repr__(self):
134 return f'<{self.__class__.__name__}={self._file_like_reader}>'
@@ -0,0 +1,40 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import os
19
20
21 def get_config(ini_path, **kwargs):
22 import configparser
23 parser = configparser.ConfigParser(**kwargs)
24 parser.read(ini_path)
25 return parser
26
27
28 def get_app_config_lightweight(ini_path):
29 parser = get_config(ini_path)
30 parser.set('app:main', 'here', os.getcwd())
31 parser.set('app:main', '__file__', ini_path)
32 return dict(parser.items('app:main'))
33
34
35 def get_app_config(ini_path):
36 """
37 This loads the app context and provides a heavy type iniliaziation of config
38 """
39 from paste.deploy.loadwsgi import appconfig
40 return appconfig(f'config:{ini_path}', relative_to=os.getcwd())
@@ -0,0 +1,50 b''
1
2
3 import logging
4 from sqlalchemy import *
5 from sqlalchemy.engine import reflection
6
7 from alembic.migration import MigrationContext
8 from alembic.operations import Operations
9
10 from rhodecode.lib.dbmigrate.versions import _reset_base
11 from rhodecode.model import meta, init_model_encryption
12
13
14 log = logging.getLogger(__name__)
15
16
17 def _get_indexes_list(migrate_engine, table_name):
18 inspector = reflection.Inspector.from_engine(migrate_engine)
19 return inspector.get_indexes(table_name)
20
21
22 def upgrade(migrate_engine):
23 """
24 Upgrade operations go here.
25 Don't create your own engine; bind migrate_engine to your metadata
26 """
27 from rhodecode.model import db as db_5_1_0_0
28
29 # issue fixups
30 fixups(db_5_1_0_0, meta.Session)
31
32
33 def downgrade(migrate_engine):
34 pass
35
36
37 def fixups(models, _SESSION):
38 for db_repo in _SESSION.query(models.Repository).all():
39
40 config = db_repo._config
41 config.set('extensions', 'largefiles', '')
42
43 try:
44 scm = db_repo.scm_instance(cache=False, config=config)
45 if scm:
46 print(f'installing hook for repo: {db_repo}')
47 scm.install_hooks(force=True)
48 except Exception as e:
49 print(e)
50 print('continue...')
@@ -0,0 +1,17 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,89 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import time
21 import logging
22
23 from rhodecode.lib.config_utils import get_config
24
25 from rhodecode.lib.svn_txn_utils import get_txn_id_from_store
26
27 log = logging.getLogger(__name__)
28
29
30 class BaseHooksCallbackDaemon:
31 """
32 Basic context manager for actions that don't require some extra
33 """
34 def __init__(self):
35 pass
36
37 def __enter__(self):
38 log.debug('Running `%s` callback daemon', self.__class__.__name__)
39 return self
40
41 def __exit__(self, exc_type, exc_val, exc_tb):
42 log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
43
44
45 class HooksModuleCallbackDaemon(BaseHooksCallbackDaemon):
46
47 def __init__(self, module):
48 super().__init__()
49 self.hooks_module = module
50
51 def __repr__(self):
52 return f'HooksModuleCallbackDaemon(hooks_module={self.hooks_module})'
53
54
55 def prepare_callback_daemon(extras, protocol, host, txn_id=None):
56
57 match protocol:
58 case 'http':
59 from rhodecode.lib.hook_daemon.http_hooks_deamon import HttpHooksCallbackDaemon
60 port = 0
61 if txn_id:
62 # read txn-id to re-use the PORT for callback daemon
63 repo_path = os.path.join(extras['repo_store'], extras['repository'])
64 txn_details = get_txn_id_from_store(repo_path, txn_id)
65 port = txn_details.get('port', 0)
66
67 callback_daemon = HttpHooksCallbackDaemon(
68 txn_id=txn_id, host=host, port=port)
69 case 'celery':
70 from rhodecode.lib.hook_daemon.celery_hooks_deamon import CeleryHooksCallbackDaemon
71 callback_daemon = CeleryHooksCallbackDaemon(get_config(extras['config']))
72 case 'local':
73 from rhodecode.lib.hook_daemon.hook_module import Hooks
74 callback_daemon = HooksModuleCallbackDaemon(Hooks.__module__)
75 case _:
76 log.error('Unsupported callback daemon protocol "%s"', protocol)
77 raise Exception('Unsupported callback daemon protocol.')
78
79 extras['hooks_uri'] = getattr(callback_daemon, 'hooks_uri', '')
80 extras['task_queue'] = getattr(callback_daemon, 'task_queue', '')
81 extras['task_backend'] = getattr(callback_daemon, 'task_backend', '')
82 extras['hooks_protocol'] = protocol
83 extras['time'] = time.time()
84
85 # register txn_id
86 extras['txn_id'] = txn_id
87 log.debug('Prepared a callback daemon: %s',
88 callback_daemon.__class__.__name__)
89 return callback_daemon, extras
@@ -0,0 +1,33 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 from rhodecode.lib.hook_daemon.base import BaseHooksCallbackDaemon
20
21
22 class CeleryHooksCallbackDaemon(BaseHooksCallbackDaemon):
23 """
24 Context manger for achieving a compatibility with celery backend
25 """
26
27 def __init__(self, config):
28 # TODO: replace this with settings bootstrapped...
29 self.task_queue = config.get('app:main', 'celery.broker_url')
30 self.task_backend = config.get('app:main', 'celery.result_backend')
31
32 def __repr__(self):
33 return f'CeleryHooksCallbackDaemon(task_queue={self.task_queue}, task_backend={self.task_backend})'
@@ -0,0 +1,104 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import logging
20 import traceback
21
22 from rhodecode.model import meta
23
24 from rhodecode.lib import hooks_base
25 from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected
26 from rhodecode.lib.utils2 import AttributeDict
27
28 log = logging.getLogger(__name__)
29
30
31 class Hooks(object):
32 """
33 Exposes the hooks for remote callbacks
34 """
35 def __init__(self, request=None, log_prefix=''):
36 self.log_prefix = log_prefix
37 self.request = request
38
39 def repo_size(self, extras):
40 log.debug("%sCalled repo_size of %s object", self.log_prefix, self)
41 return self._call_hook(hooks_base.repo_size, extras)
42
43 def pre_pull(self, extras):
44 log.debug("%sCalled pre_pull of %s object", self.log_prefix, self)
45 return self._call_hook(hooks_base.pre_pull, extras)
46
47 def post_pull(self, extras):
48 log.debug("%sCalled post_pull of %s object", self.log_prefix, self)
49 return self._call_hook(hooks_base.post_pull, extras)
50
51 def pre_push(self, extras):
52 log.debug("%sCalled pre_push of %s object", self.log_prefix, self)
53 return self._call_hook(hooks_base.pre_push, extras)
54
55 def post_push(self, extras):
56 log.debug("%sCalled post_push of %s object", self.log_prefix, self)
57 return self._call_hook(hooks_base.post_push, extras)
58
59 def _call_hook(self, hook, extras):
60 extras = AttributeDict(extras)
61 _server_url = extras['server_url']
62
63 extras.request = self.request
64
65 try:
66 result = hook(extras)
67 if result is None:
68 raise Exception(f'Failed to obtain hook result from func: {hook}')
69 except HTTPBranchProtected as handled_error:
70 # Those special cases don't need error reporting. It's a case of
71 # locked repo or protected branch
72 result = AttributeDict({
73 'status': handled_error.code,
74 'output': handled_error.explanation
75 })
76 except (HTTPLockedRC, Exception) as error:
77 # locked needs different handling since we need to also
78 # handle PULL operations
79 exc_tb = ''
80 if not isinstance(error, HTTPLockedRC):
81 exc_tb = traceback.format_exc()
82 log.exception('%sException when handling hook %s', self.log_prefix, hook)
83 error_args = error.args
84 return {
85 'status': 128,
86 'output': '',
87 'exception': type(error).__name__,
88 'exception_traceback': exc_tb,
89 'exception_args': error_args,
90 }
91 finally:
92 meta.Session.remove()
93
94 log.debug('%sGot hook call response %s', self.log_prefix, result)
95 return {
96 'status': result.status,
97 'output': result.output,
98 }
99
100 def __enter__(self):
101 return self
102
103 def __exit__(self, exc_type, exc_val, exc_tb):
104 pass
@@ -0,0 +1,287 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import logging
21 import traceback
22 import threading
23 import socket
24 import msgpack
25 import gevent
26
27 from http.server import BaseHTTPRequestHandler
28 from socketserver import TCPServer
29
30 from rhodecode.model import meta
31 from rhodecode.lib.ext_json import json
32 from rhodecode.lib import rc_cache
33 from rhodecode.lib.svn_txn_utils import get_txn_id_data_key
34 from rhodecode.lib.hook_daemon.hook_module import Hooks
35
36 log = logging.getLogger(__name__)
37
38
39 class HooksHttpHandler(BaseHTTPRequestHandler):
40
41 JSON_HOOKS_PROTO = 'json.v1'
42 MSGPACK_HOOKS_PROTO = 'msgpack.v1'
43 # starting with RhodeCode 5.0.0 MsgPack is the default, prior it used json
44 DEFAULT_HOOKS_PROTO = MSGPACK_HOOKS_PROTO
45
46 @classmethod
47 def serialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO):
48 if proto == cls.MSGPACK_HOOKS_PROTO:
49 return msgpack.packb(data)
50 return json.dumps(data)
51
52 @classmethod
53 def deserialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO):
54 if proto == cls.MSGPACK_HOOKS_PROTO:
55 return msgpack.unpackb(data)
56 return json.loads(data)
57
58 def do_POST(self):
59 hooks_proto, method, extras = self._read_request()
60 log.debug('Handling HooksHttpHandler %s with %s proto', method, hooks_proto)
61
62 txn_id = getattr(self.server, 'txn_id', None)
63 if txn_id:
64 log.debug('Computing TXN_ID based on `%s`:`%s`',
65 extras['repository'], extras['txn_id'])
66 computed_txn_id = rc_cache.utils.compute_key_from_params(
67 extras['repository'], extras['txn_id'])
68 if txn_id != computed_txn_id:
69 raise Exception(
70 'TXN ID fail: expected {} got {} instead'.format(
71 txn_id, computed_txn_id))
72
73 request = getattr(self.server, 'request', None)
74 try:
75 hooks = Hooks(request=request, log_prefix='HOOKS: {} '.format(self.server.server_address))
76 result = self._call_hook_method(hooks, method, extras)
77
78 except Exception as e:
79 exc_tb = traceback.format_exc()
80 result = {
81 'exception': e.__class__.__name__,
82 'exception_traceback': exc_tb,
83 'exception_args': e.args
84 }
85 self._write_response(hooks_proto, result)
86
87 def _read_request(self):
88 length = int(self.headers['Content-Length'])
89 # respect sent headers, fallback to OLD proto for compatability
90 hooks_proto = self.headers.get('rc-hooks-protocol') or self.JSON_HOOKS_PROTO
91 if hooks_proto == self.MSGPACK_HOOKS_PROTO:
92 # support for new vcsserver msgpack based protocol hooks
93 body = self.rfile.read(length)
94 data = self.deserialize_data(body)
95 else:
96 body = self.rfile.read(length)
97 data = self.deserialize_data(body)
98
99 return hooks_proto, data['method'], data['extras']
100
101 def _write_response(self, hooks_proto, result):
102 self.send_response(200)
103 if hooks_proto == self.MSGPACK_HOOKS_PROTO:
104 self.send_header("Content-type", "application/msgpack")
105 self.end_headers()
106 data = self.serialize_data(result)
107 self.wfile.write(data)
108 else:
109 self.send_header("Content-type", "text/json")
110 self.end_headers()
111 data = self.serialize_data(result)
112 self.wfile.write(data)
113
114 def _call_hook_method(self, hooks, method, extras):
115 try:
116 result = getattr(hooks, method)(extras)
117 finally:
118 meta.Session.remove()
119 return result
120
121 def log_message(self, format, *args):
122 """
123 This is an overridden method of BaseHTTPRequestHandler which logs using
124 a logging library instead of writing directly to stderr.
125 """
126
127 message = format % args
128
129 log.debug(
130 "HOOKS: client=%s - - [%s] %s", self.client_address,
131 self.log_date_time_string(), message)
132
133
134 class ThreadedHookCallbackDaemon(object):
135
136 _callback_thread = None
137 _daemon = None
138 _done = False
139 use_gevent = False
140
141 def __init__(self, txn_id=None, host=None, port=None):
142 self._prepare(txn_id=txn_id, host=host, port=port)
143 if self.use_gevent:
144 self._run_func = self._run_gevent
145 self._stop_func = self._stop_gevent
146 else:
147 self._run_func = self._run
148 self._stop_func = self._stop
149
150 def __enter__(self):
151 log.debug('Running `%s` callback daemon', self.__class__.__name__)
152 self._run_func()
153 return self
154
155 def __exit__(self, exc_type, exc_val, exc_tb):
156 log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
157 self._stop_func()
158
159 def _prepare(self, txn_id=None, host=None, port=None):
160 raise NotImplementedError()
161
162 def _run(self):
163 raise NotImplementedError()
164
165 def _stop(self):
166 raise NotImplementedError()
167
168 def _run_gevent(self):
169 raise NotImplementedError()
170
171 def _stop_gevent(self):
172 raise NotImplementedError()
173
174
175 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
176 """
177 Context manager which will run a callback daemon in a background thread.
178 """
179
180 hooks_uri = None
181
182 # From Python docs: Polling reduces our responsiveness to a shutdown
183 # request and wastes cpu at all other times.
184 POLL_INTERVAL = 0.01
185
186 use_gevent = False
187
188 def __repr__(self):
189 return f'HttpHooksCallbackDaemon(hooks_uri={self.hooks_uri})'
190
191 @property
192 def _hook_prefix(self):
193 return f'HOOKS: {self.hooks_uri} '
194
195 def get_hostname(self):
196 return socket.gethostname() or '127.0.0.1'
197
198 def get_available_port(self, min_port=20000, max_port=65535):
199 from rhodecode.lib.utils2 import get_available_port as _get_port
200 return _get_port(min_port, max_port)
201
202 def _prepare(self, txn_id=None, host=None, port=None):
203 from pyramid.threadlocal import get_current_request
204
205 if not host or host == "*":
206 host = self.get_hostname()
207 if not port:
208 port = self.get_available_port()
209
210 server_address = (host, port)
211 self.hooks_uri = f'{host}:{port}'
212 self.txn_id = txn_id
213 self._done = False
214
215 log.debug(
216 "%s Preparing HTTP callback daemon registering hook object: %s",
217 self._hook_prefix, HooksHttpHandler)
218
219 self._daemon = TCPServer(server_address, HooksHttpHandler)
220 # inject transaction_id for later verification
221 self._daemon.txn_id = self.txn_id
222
223 # pass the WEB app request into daemon
224 self._daemon.request = get_current_request()
225
226 def _run(self):
227 log.debug("Running thread-based loop of callback daemon in background")
228 callback_thread = threading.Thread(
229 target=self._daemon.serve_forever,
230 kwargs={'poll_interval': self.POLL_INTERVAL})
231 callback_thread.daemon = True
232 callback_thread.start()
233 self._callback_thread = callback_thread
234
235 def _run_gevent(self):
236 log.debug("Running gevent-based loop of callback daemon in background")
237 # create a new greenlet for the daemon's serve_forever method
238 callback_greenlet = gevent.spawn(
239 self._daemon.serve_forever,
240 poll_interval=self.POLL_INTERVAL)
241
242 # store reference to greenlet
243 self._callback_greenlet = callback_greenlet
244
245 # switch to this greenlet
246 gevent.sleep(0.01)
247
248 def _stop(self):
249 log.debug("Waiting for background thread to finish.")
250 self._daemon.shutdown()
251 self._callback_thread.join()
252 self._daemon = None
253 self._callback_thread = None
254 if self.txn_id:
255 #TODO: figure out the repo_path...
256 repo_path = ''
257 txn_id_file = get_txn_id_data_key(repo_path, self.txn_id)
258 log.debug('Cleaning up TXN ID %s', txn_id_file)
259 if os.path.isfile(txn_id_file):
260 os.remove(txn_id_file)
261
262 log.debug("Background thread done.")
263
264 def _stop_gevent(self):
265 log.debug("Waiting for background greenlet to finish.")
266
267 # if greenlet exists and is running
268 if self._callback_greenlet and not self._callback_greenlet.dead:
269 # shutdown daemon if it exists
270 if self._daemon:
271 self._daemon.shutdown()
272
273 # kill the greenlet
274 self._callback_greenlet.kill()
275
276 self._daemon = None
277 self._callback_greenlet = None
278
279 if self.txn_id:
280 #TODO: figure out the repo_path...
281 repo_path = ''
282 txn_id_file = get_txn_id_data_key(repo_path, self.txn_id)
283 log.debug('Cleaning up TXN ID %s', txn_id_file)
284 if os.path.isfile(txn_id_file):
285 os.remove(txn_id_file)
286
287 log.debug("Background greenlet done.")
@@ -0,0 +1,132 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import logging
20 import redis
21
22 from ..lib import rc_cache
23 from ..lib.ext_json import json
24
25
26 log = logging.getLogger(__name__)
27
28 redis_client = None
29
30
31 class RedisTxnClient:
32
33 def __init__(self, url):
34 self.url = url
35 self._create_client(url)
36
37 def _create_client(self, url):
38 connection_pool = redis.ConnectionPool.from_url(url)
39 self.writer_client = redis.StrictRedis(
40 connection_pool=connection_pool
41 )
42 self.reader_client = self.writer_client
43
44 def set(self, key, value, expire=24 * 60000):
45 self.writer_client.set(key, value, ex=expire)
46
47 def get(self, key):
48 return self.reader_client.get(key)
49
50 def delete(self, key):
51 self.writer_client.delete(key)
52
53
54 def get_redis_client(url=''):
55
56 global redis_client
57 if redis_client is not None:
58 return redis_client
59 if not url:
60 from rhodecode import CONFIG
61 url = CONFIG['vcs.svn.redis_conn']
62 redis_client = RedisTxnClient(url)
63 return redis_client
64
65
66 def extract_svn_txn_id(data: bytes):
67 """
68 Helper method for extraction of svn txn_id from submitted XML data during
69 POST operations
70 """
71 import re
72 from lxml import etree
73
74 try:
75 root = etree.fromstring(data)
76 pat = re.compile(r'/txn/(?P<txn_id>.*)')
77 for el in root:
78 if el.tag == '{DAV:}source':
79 for sub_el in el:
80 if sub_el.tag == '{DAV:}href':
81 match = pat.search(sub_el.text)
82 if match:
83 svn_tx_id = match.groupdict()['txn_id']
84 return svn_tx_id
85 except Exception:
86 log.exception('Failed to extract txn_id')
87
88
89 def get_txn_id_data_key(repo_path, svn_txn_id):
90 log.debug('svn-txn-id: %s, obtaining data path', svn_txn_id)
91 repo_key = rc_cache.utils.compute_key_from_params(repo_path)
92 final_key = f'{repo_key}.{svn_txn_id}.svn_txn_id'
93 log.debug('computed final key: %s', final_key)
94
95 return final_key
96
97
98 def store_txn_id_data(repo_path, svn_txn_id, data_dict):
99 log.debug('svn-txn-id: %s, storing data', svn_txn_id)
100
101 if not svn_txn_id:
102 log.warning('Cannot store txn_id because it is empty')
103 return
104
105 redis_conn = get_redis_client()
106
107 store_key = get_txn_id_data_key(repo_path, svn_txn_id)
108 store_data = json.dumps(data_dict)
109 redis_conn.set(store_key, store_data)
110
111
112 def get_txn_id_from_store(repo_path, svn_txn_id, rm_on_read=False):
113 """
114 Reads txn_id from store and if present returns the data for callback manager
115 """
116 log.debug('svn-txn-id: %s, retrieving data', svn_txn_id)
117 redis_conn = get_redis_client()
118
119 store_key = get_txn_id_data_key(repo_path, svn_txn_id)
120 data = {}
121 redis_conn.get(store_key)
122 try:
123 raw_data = redis_conn.get(store_key)
124 data = json.loads(raw_data)
125 except Exception:
126 log.exception('Failed to get txn_id metadata')
127
128 if rm_on_read:
129 log.debug('Cleaning up txn_id at %s', store_key)
130 redis_conn.delete(store_key)
131
132 return data
@@ -0,0 +1,134 b''
1 <%namespace name="base" file="/base/base.mako"/>
2
3 <div class="panel panel-default">
4 <div class="panel-heading">
5 <h3 class="panel-title">${_('Enable/Disable 2FA for your account')}</h3>
6 </div>
7 ${h.secure_form(h.route_path('my_account_configure_2fa_update'), request=request)}
8 <div class="panel-body">
9 <div class="form">
10 <div class="fields">
11 <div class="field">
12 <div class="label">
13 <label>${_('2FA status')}:</label>
14 </div>
15 <div class="checkboxes">
16 % if c.locked_2fa:
17 <span class="help-block">${_('2FA settings cannot be changed here, because 2FA was forced enabled by RhodeCode Administrator.')}</span>
18
19 % else:
20 <div class="form-check">
21 <input type="radio" id="2faEnabled" name="2fa_status" value="1" ${'checked=1' if c.state_of_2fa else ''}/>
22 <label for="2faEnabled">${_('Enable 2FA')}</label>
23
24 <input type="radio" id="2faDisabled" name="2fa_status" value="0" ${'checked=1' if not c.state_of_2fa else ''} />
25 <label for="2faDisabled">${_('Disable 2FA')}</label>
26 </div>
27 % endif
28
29 </div>
30 </div>
31 </div>
32 <button id="saveBtn" class="btn btn-primary" ${'disabled' if c.locked_2fa else ''}>${_('Save')}</button>
33 </div>
34 </div>
35 ${h.end_form()}
36 </div>
37
38 % if c.state_of_2fa:
39
40
41 % if not c.user_seen_2fa_recovery_codes:
42
43 <div class="panel panel-warning">
44 <div class="panel-heading" id="advanced-archive">
45 <h3 class="panel-title">${_('2FA Recovery codes')} <a class="permalink" href="#advanced-archive"> ¶</a></h3>
46 </div>
47 <div class="panel-body">
48 <p>
49 ${_('You have not seen your 2FA recovery codes yet.')}
50 ${_('Please save them in a safe place, or you will lose access to your account in case of lost access to authenticator app.')}
51 </p>
52 <br/>
53 <a href="${request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1})}" class="btn btn-primary">${_('Show recovery codes')}</a>
54 </div>
55 </div>
56 % endif
57
58
59 ${h.secure_form(h.route_path('my_account_regenerate_2fa_recovery_codes'), request=request)}
60 <div class="panel panel-default">
61 <div class="panel-heading">
62 <h3 class="panel-title">${_('Regenerate 2FA recovery codes for your account')}</h3>
63 </div>
64 <div class="panel-body">
65 <form id="2faForm">
66 <input type="text" name="totp" placeholder="${_('Verify the code from the app')}" pattern="\d{6}" style="width: 20%">
67 <button type="submit" class="btn btn-primary">${_('Verify and generate new codes')}</button>
68 </form>
69 </div>
70
71 </div>
72 ${h.end_form()}
73 % endif
74
75
76 <script>
77
78 function showRecoveryCodesPopup() {
79
80 SwalNoAnimation.fire({
81 title: _gettext('2FA recovery codes'),
82 html: '<span>Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you will lose access to your account.</span>',
83 showCancelButton: false,
84 showConfirmButton: true,
85 showLoaderOnConfirm: true,
86 confirmButtonText: _gettext('Show now'),
87 allowOutsideClick: function () {
88 !Swal.isLoading()
89 },
90
91 preConfirm: function () {
92
93 var postData = {
94 'csrf_token': CSRF_TOKEN
95 };
96 return new Promise(function (resolve, reject) {
97 $.ajax({
98 type: 'POST',
99 data: postData,
100 url: pyroutes.url('my_account_show_2fa_recovery_codes'),
101 headers: {'X-PARTIAL-XHR': true}
102 })
103 .done(function (data) {
104 resolve(data);
105 })
106 .fail(function (jqXHR, textStatus, errorThrown) {
107 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
108 ajaxErrorSwal(message);
109 });
110 })
111 }
112
113 })
114 .then(function (result) {
115 if (result.value) {
116 let funcData = {'recoveryCodes': result.value.recovery_codes}
117 let recoveryCodesHtml = renderTemplate('recoveryCodes', funcData);
118 SwalNoAnimation.fire({
119 allowOutsideClick: false,
120 confirmButtonText: _gettext('I Copied the codes'),
121 title: _gettext('2FA Recovery Codes'),
122 html: recoveryCodesHtml
123 }).then(function (result) {
124 if (result.isConfirmed) {
125 window.location.reload()
126 }
127 })
128 }
129 })
130 }
131 % if request.GET.get('show-recovery-codes') == '1' and not c.user_seen_2fa_recovery_codes:
132 showRecoveryCodesPopup();
133 % endif
134 </script>
@@ -0,0 +1,90 b''
1 <%inherit file="base/root.mako"/>
2
3 <%def name="title()">
4 ${_('Setup 2FA')}
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
8 </%def>
9
10 <style>body{background-color:#eeeeee;}</style>
11
12 <div class="loginbox" style="width: 600px">
13
14 <div class="header-account">
15 <div id="header-inner" class="title">
16 <div id="logo">
17 % if c.rhodecode_name:
18 <div class="branding">
19 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
20 </div>
21 % endif
22 </div>
23 </div>
24 </div>
25
26 <div class="loginwrapper">
27 <rhodecode-toast id="notifications"></rhodecode-toast>
28
29 <div class="sign-in-title">
30 <h1>${_('Set up the authenticator app')} - ${_('scan the QR code')}</h1>
31 </div>
32 <div class="inner form">
33 ${h.secure_form(h.route_path('setup_2fa'), request=request, id='totp_form')}
34 <strong>${_('Use an authenticator app to scan.')}</strong><br/>
35
36 ## QR CODE
37 <code>${_('Account')}: ${totp_name}</code><br/>
38 <div class="qr-code-container">
39 <img alt="qr-code" src="data:image/png;base64, ${qr}"/>
40 </div>
41
42 <div id="alternativeCode" style="margin: -10px 0 5px 0">${_('Unable to scan?')} <a id="toggleLink">${_('Click here')}</a></div>
43
44 ## Secret alternative code
45 <div id="secretDiv" style="display: none">
46
47 <div style="padding: 10px 0">
48 <strong style="padding: 4px 0">${_('Copy and use this code to manually set up an authenticator app')}</strong>
49 <code>${key}</code><i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${key}" title="${_('Copy the secret key')}" ></i><br/>
50 <code>${_('type')}: time-based</code>
51 </div>
52
53 </div>
54
55 <label for="totp">${_('Verify the code from the app')}:</label>
56 ${h.text('totp', class_='form-control', )}
57 <div id="formErrors">
58 % if 'totp' in errors:
59 <span class="error-message">${errors.get('totp')}</span>
60 <br />
61 % endif
62 % if 'secret_totp' in errors:
63 <span class="error-message">SECRET:${errors.get('secret_totp')}</span>
64 <br />
65 % endif
66 </div>
67 ${h.hidden('secret_totp', key)}
68 ${h.submit('verify_2fa',_('Verify'), class_="btn sign-in")}
69
70 ${h.end_form()}
71 </div>
72
73 </div>
74
75 </div>
76
77 <script type="text/javascript">
78
79 $(document).ready(function() {
80
81 $( "#toggleLink" ).on("click", function() {
82 $( "#secretDiv" ).toggle();
83 $( "#alternativeCode").hide();
84 $('#totp').focus();
85 });
86
87 $('#totp').focus();
88 })
89
90 </script>
@@ -0,0 +1,54 b''
1 <%inherit file="base/root.mako"/>
2
3 <%def name="title()">
4 ${_('Verify 2FA')}
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
8 </%def>
9 <style>body{background-color:#eeeeee;}</style>
10
11 <div class="loginbox" style="width: 600px">
12 <div class="header-account">
13 <div id="header-inner" class="title">
14 <div id="logo">
15 % if c.rhodecode_name:
16 <div class="branding">
17 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
18 </div>
19 % endif
20 </div>
21 </div>
22 </div>
23
24 <div class="loginwrapper">
25 <rhodecode-toast id="notifications"></rhodecode-toast>
26
27 <div id="register">
28 <div class="sign-in-title">
29 <h1>${_('Verify the code from the app')}</h1>
30 </div>
31 <div class="inner form">
32 ${h.secure_form(h.route_path('check_2fa'), request=request, id='totp_form')}
33 <label for="totp">${_('Verification code')}:</label>
34 ${h.text('totp', class_="form-control")}
35 %if 'totp' in errors:
36 <span class="error-message">${errors.get('totp')}</span>
37 <br />
38 %endif
39 <p class="help-block">${_('Enter the code from your two-factor authenticator app. If you\'ve lost your device, you can enter one of your recovery codes.')}</p>
40
41 ${h.submit('send', _('Verify'), class_="btn sign-in")}
42 <p class="help-block pull-right">
43 RhodeCode ${c.rhodecode_edition}
44 </p>
45 ${h.end_form()}
46 </div>
47 </div>
48
49 </div>
50 </div>
51
52
53
54
@@ -0,0 +1,105 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import time
20 import pytest
21 import rhodecode
22 import os
23 import shutil
24 from tempfile import mkdtemp
25
26 from rhodecode.lib import archive_cache
27
28
29 def file_reader(temp_store):
30 with open(temp_store, 'w') as f:
31 for cnt in range(10000):
32 f.write(str(cnt))
33 return open(temp_store, 'rb')
34
35
36 @pytest.fixture()
37 def d_cache_instance(ini_settings):
38 config = ini_settings
39 d_cache = archive_cache.get_archival_cache_store(config=config, always_init=True)
40 return d_cache
41
42
43 @pytest.mark.usefixtures('app')
44 class TestArchiveCaches(object):
45
46 def test_archivecache_empty_stats(self, d_cache_instance):
47 d_cache = d_cache_instance
48 shutil.rmtree(d_cache._directory)
49
50 stats = d_cache.get_statistics()
51 assert (0, 0, {}) == stats
52
53 def test_archivecache_store_keys(self, d_cache_instance, tmp_path):
54 d_cache = d_cache_instance
55 shutil.rmtree(d_cache._directory)
56
57 for n in range(100):
58
59 archive_name = f'my-archive-abc-{n}.zip'
60 temp_archive_path = os.path.join(tmp_path, archive_name)
61 d_cache.store(archive_name, file_reader(temp_archive_path ), {'foo': 'bar'})
62 reader, meta = d_cache.fetch(archive_name)
63 content = reader.read()
64 assert content == open(temp_archive_path, 'rb').read()
65
66 stats = d_cache.get_statistics()
67 assert (100, 3889000, {}) == stats
68
69 def test_archivecache_remove_keys(self, d_cache_instance, tmp_path):
70 d_cache = d_cache_instance
71 shutil.rmtree(d_cache._directory)
72
73 n = 1
74 archive_name = f'my-archive-abc-{n}.zip'
75 temp_archive_path = os.path.join(tmp_path, archive_name)
76
77 d_cache.store(archive_name, file_reader(temp_archive_path ), {'foo': 'bar'})
78 stats = d_cache.get_statistics()
79 assert (1, 38890, {}) == stats
80
81 assert 1 == d_cache.remove(archive_name)
82
83 stats = d_cache.get_statistics()
84 assert (0, 0, {}) == stats
85
86 def test_archivecache_evict_keys(self, d_cache_instance, tmp_path):
87 d_cache = d_cache_instance
88 shutil.rmtree(d_cache._directory)
89 tries = 500
90 for n in range(tries):
91
92 archive_name = f'my-archive-abc-{n}.zip'
93 temp_archive_path = os.path.join(tmp_path, archive_name)
94 d_cache.store(archive_name, file_reader(temp_archive_path ), {'foo': 'bar'})
95
96 stats = d_cache.get_statistics()
97 assert (tries, 19445000, {}) == stats
98 evict_to = 0.005 # around (5mb)
99 evicted_items = d_cache.evict(size_limit=d_cache.gb_to_bytes(evict_to))
100 evicted = 361
101 assert {'removed_items': evicted, 'removed_size': 14039290} == evicted_items
102
103 stats = d_cache.get_statistics()
104 assert (tries - evicted, 5405710, {}) == stats
105
@@ -0,0 +1,226 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
20 """
21 Test suite for making push/pull operations, on specially modified INI files
22
23 .. important::
24
25 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
26 to redirect things to stderr instead of stdout.
27 """
28
29
30 import time
31
32 import pytest
33
34 from rhodecode.lib import rc_cache
35 from rhodecode.model.db import Repository, UserIpMap, CacheKey
36 from rhodecode.model.meta import Session
37 from rhodecode.model.repo import RepoModel
38 from rhodecode.model.user import UserModel
39 from rhodecode.tests import (GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN)
40
41 from rhodecode.tests.vcs_operations import (
42 Command, _check_proper_clone, _add_files_and_push, HG_REPO_WITH_GROUP)
43
44
45 @pytest.mark.usefixtures("disable_locking", "disable_anonymous_user")
46 class TestVCSOperations(object):
47
48 def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir):
49 clone_url = rc_web_server.repo_clone_url(HG_REPO)
50 stdout, stderr = Command('/tmp').execute(
51 'hg clone', clone_url, tmpdir.strpath)
52 _check_proper_clone(stdout, stderr, 'hg')
53
54 def test_clone_hg_repo_by_admin_pull_protocol(self, rc_web_server, tmpdir):
55 clone_url = rc_web_server.repo_clone_url(HG_REPO)
56 stdout, stderr = Command('/tmp').execute(
57 'hg clone --pull', clone_url, tmpdir.strpath)
58 _check_proper_clone(stdout, stderr, 'hg')
59
60 def test_clone_hg_repo_by_admin_pull_stream_protocol(self, rc_web_server, tmpdir):
61 clone_url = rc_web_server.repo_clone_url(HG_REPO)
62 stdout, stderr = Command('/tmp').execute(
63 'hg clone --pull --stream', clone_url, tmpdir.strpath)
64 assert 'files to transfer,' in stdout
65 assert 'transferred 1.' in stdout
66 assert '114 files updated,' in stdout
67
68 def test_clone_hg_repo_by_id_by_admin(self, rc_web_server, tmpdir):
69 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
70 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
71 stdout, stderr = Command('/tmp').execute(
72 'hg clone', clone_url, tmpdir.strpath)
73 _check_proper_clone(stdout, stderr, 'hg')
74
75 def test_clone_hg_repo_with_group_by_admin(self, rc_web_server, tmpdir):
76 clone_url = rc_web_server.repo_clone_url(HG_REPO_WITH_GROUP)
77 stdout, stderr = Command('/tmp').execute(
78 'hg clone', clone_url, tmpdir.strpath)
79 _check_proper_clone(stdout, stderr, 'hg')
80
81 def test_clone_wrong_credentials_hg(self, rc_web_server, tmpdir):
82 clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!')
83 stdout, stderr = Command('/tmp').execute(
84 'hg clone', clone_url, tmpdir.strpath)
85 assert 'abort: authorization failed' in stderr
86
87 def test_clone_git_dir_as_hg(self, rc_web_server, tmpdir):
88 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
89 stdout, stderr = Command('/tmp').execute(
90 'hg clone', clone_url, tmpdir.strpath)
91 assert 'HTTP Error 404: Not Found' in stderr
92
93 def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir):
94 clone_url = rc_web_server.repo_clone_url('trololo')
95 stdout, stderr = Command('/tmp').execute(
96 'hg clone', clone_url, tmpdir.strpath)
97 assert 'HTTP Error 404: Not Found' in stderr
98
99 def test_clone_hg_with_slashes(self, rc_web_server, tmpdir):
100 clone_url = rc_web_server.repo_clone_url('//' + HG_REPO)
101 stdout, stderr = Command('/tmp').execute('hg clone', clone_url, tmpdir.strpath)
102 assert 'HTTP Error 404: Not Found' in stderr
103
104 def test_clone_existing_path_hg_not_in_database(
105 self, rc_web_server, tmpdir, fs_repo_only):
106
107 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
108 clone_url = rc_web_server.repo_clone_url(db_name)
109 stdout, stderr = Command('/tmp').execute(
110 'hg clone', clone_url, tmpdir.strpath)
111 assert 'HTTP Error 404: Not Found' in stderr
112
113 def test_clone_existing_path_hg_not_in_database_different_scm(
114 self, rc_web_server, tmpdir, fs_repo_only):
115 db_name = fs_repo_only('not-in-db-git', repo_type='git')
116 clone_url = rc_web_server.repo_clone_url(db_name)
117 stdout, stderr = Command('/tmp').execute(
118 'hg clone', clone_url, tmpdir.strpath)
119 assert 'HTTP Error 404: Not Found' in stderr
120
121 def test_clone_non_existing_store_path_hg(self, rc_web_server, tmpdir, user_util):
122 repo = user_util.create_repo()
123 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
124
125 # Damage repo by removing it's folder
126 RepoModel()._delete_filesystem_repo(repo)
127
128 stdout, stderr = Command('/tmp').execute(
129 'hg clone', clone_url, tmpdir.strpath)
130 assert 'HTTP Error 404: Not Found' in stderr
131
132 def test_push_new_file_hg(self, rc_web_server, tmpdir):
133 clone_url = rc_web_server.repo_clone_url(HG_REPO)
134 stdout, stderr = Command('/tmp').execute(
135 'hg clone', clone_url, tmpdir.strpath)
136
137 stdout, stderr = _add_files_and_push(
138 'hg', tmpdir.strpath, clone_url=clone_url)
139
140 assert 'pushing to' in stdout
141 assert 'size summary' in stdout
142
143 def test_push_invalidates_cache(self, rc_web_server, tmpdir):
144 hg_repo = Repository.get_by_repo_name(HG_REPO)
145
146 # init cache objects
147 CacheKey.delete_all_cache()
148
149 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=hg_repo.repo_id)
150
151 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key)
152
153 with inv_context_manager as invalidation_context:
154 # __enter__ will create and register cache objects
155 pass
156
157 cache_keys = hg_repo.cache_keys
158 assert cache_keys != []
159 old_ids = [x.cache_state_uid for x in cache_keys]
160
161 # clone to init cache
162 clone_url = rc_web_server.repo_clone_url(hg_repo.repo_name)
163 stdout, stderr = Command('/tmp').execute(
164 'hg clone', clone_url, tmpdir.strpath)
165
166 cache_keys = hg_repo.cache_keys
167 assert cache_keys != []
168 for key in cache_keys:
169 assert key.cache_active is True
170
171 # PUSH that should trigger invalidation cache
172 stdout, stderr = _add_files_and_push(
173 'hg', tmpdir.strpath, clone_url=clone_url, files_no=1)
174
175 # flush...
176 Session().commit()
177 hg_repo = Repository.get_by_repo_name(HG_REPO)
178 cache_keys = hg_repo.cache_keys
179 assert cache_keys != []
180 new_ids = [x.cache_state_uid for x in cache_keys]
181 assert new_ids != old_ids
182
183 def test_push_wrong_credentials_hg(self, rc_web_server, tmpdir):
184 clone_url = rc_web_server.repo_clone_url(HG_REPO)
185 stdout, stderr = Command('/tmp').execute(
186 'hg clone', clone_url, tmpdir.strpath)
187
188 push_url = rc_web_server.repo_clone_url(
189 HG_REPO, user='bad', passwd='name')
190 stdout, stderr = _add_files_and_push(
191 'hg', tmpdir.strpath, clone_url=push_url)
192
193 assert 'abort: authorization failed' in stderr
194
195 def test_push_back_to_wrong_url_hg(self, rc_web_server, tmpdir):
196 clone_url = rc_web_server.repo_clone_url(HG_REPO)
197 stdout, stderr = Command('/tmp').execute(
198 'hg clone', clone_url, tmpdir.strpath)
199
200 stdout, stderr = _add_files_and_push(
201 'hg', tmpdir.strpath,
202 clone_url=rc_web_server.repo_clone_url('not-existing'))
203
204 assert 'HTTP Error 404: Not Found' in stderr
205
206 def test_ip_restriction_hg(self, rc_web_server, tmpdir):
207 user_model = UserModel()
208 try:
209 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
210 Session().commit()
211 time.sleep(2)
212 clone_url = rc_web_server.repo_clone_url(HG_REPO)
213 stdout, stderr = Command('/tmp').execute(
214 'hg clone', clone_url, tmpdir.strpath)
215 assert 'abort: HTTP Error 403: Forbidden' in stderr
216 finally:
217 # release IP restrictions
218 for ip in UserIpMap.getAll():
219 UserIpMap.delete(ip.ip_id)
220 Session().commit()
221
222 time.sleep(2)
223
224 stdout, stderr = Command('/tmp').execute(
225 'hg clone', clone_url, tmpdir.strpath)
226 _check_proper_clone(stdout, stderr, 'hg')
@@ -0,0 +1,224 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 """
20 Test suite for making push/pull operations, on specially modified INI files
21
22 .. important::
23
24 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
25 to redirect things to stderr instead of stdout.
26 """
27
28
29 import time
30 import pytest
31
32 from rhodecode.model.db import Repository, UserIpMap
33 from rhodecode.model.meta import Session
34 from rhodecode.model.repo import RepoModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import (SVN_REPO, TEST_USER_ADMIN_LOGIN)
37
38
39 from rhodecode.tests.vcs_operations import (
40 Command, _check_proper_clone, _check_proper_svn_push,
41 _add_files_and_push, SVN_REPO_WITH_GROUP)
42
43
44 def get_cli_flags(username, password):
45 flags = '--no-auth-cache --non-interactive'
46 auth = ''
47 if username and password:
48 auth = f'--username {username} --password {password}'
49 return flags, auth
50
51
52 @pytest.mark.usefixtures("disable_locking", "disable_anonymous_user")
53 class TestVCSOperations(object):
54
55 def test_clone_svn_repo_by_admin(self, rc_web_server, tmpdir):
56 clone_url = rc_web_server.repo_clone_url(SVN_REPO)
57 username, password = rc_web_server.repo_clone_credentials()
58
59 cmd = Command('/tmp')
60
61 flags, auth = get_cli_flags(username, password)
62
63 stdout, stderr = Command('/tmp').execute(
64 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
65
66 _check_proper_clone(stdout, stderr, 'svn')
67 cmd.assert_returncode_success()
68
69 def test_clone_svn_repo_by_id_by_admin(self, rc_web_server, tmpdir):
70 repo_id = Repository.get_by_repo_name(SVN_REPO).repo_id
71 username, password = rc_web_server.repo_clone_credentials()
72
73 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
74 cmd = Command('/tmp')
75
76 flags, auth = get_cli_flags(username, password)
77
78 stdout, stderr = Command('/tmp').execute(
79 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
80
81 _check_proper_clone(stdout, stderr, 'svn')
82 cmd.assert_returncode_success()
83
84 def test_clone_svn_repo_with_group_by_admin(self, rc_web_server, tmpdir):
85 clone_url = rc_web_server.repo_clone_url(SVN_REPO_WITH_GROUP)
86 username, password = rc_web_server.repo_clone_credentials()
87
88 flags, auth = get_cli_flags(username, password)
89
90 stdout, stderr = Command('/tmp').execute(
91 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
92
93 _check_proper_clone(stdout, stderr, 'svn')
94 cmd.assert_returncode_success()
95
96 def test_clone_wrong_credentials_svn(self, rc_web_server, tmpdir):
97 clone_url = rc_web_server.repo_clone_url(SVN_REPO)
98 username, password = rc_web_server.repo_clone_credentials()
99 password = 'bad-password'
100
101 flags, auth = get_cli_flags(username, password)
102
103 stdout, stderr = Command('/tmp').execute(
104 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
105 assert 'fatal: Authentication failed' in stderr
106
107 def test_clone_svn_with_slashes(self, rc_web_server, tmpdir):
108 clone_url = rc_web_server.repo_clone_url('//' + SVN_REPO)
109 username, password = '', ''
110 flags, auth = get_cli_flags(username, password)
111
112 stdout, stderr = Command('/tmp').execute(
113 f'svn checkout {flags} {auth}', clone_url)
114
115 assert 'not found' in stderr
116
117 def test_clone_existing_path_svn_not_in_database(
118 self, rc_web_server, tmpdir, fs_repo_only):
119 db_name = fs_repo_only('not-in-db-git', repo_type='git')
120 clone_url = rc_web_server.repo_clone_url(db_name)
121 username, password = '', ''
122 flags, auth = get_cli_flags(username, password)
123
124 stdout, stderr = Command('/tmp').execute(
125 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
126 assert 'not found' in stderr
127
128 def test_clone_existing_path_svn_not_in_database_different_scm(
129 self, rc_web_server, tmpdir, fs_repo_only):
130 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
131 clone_url = rc_web_server.repo_clone_url(db_name)
132
133 username, password = '', ''
134 flags, auth = get_cli_flags(username, password)
135
136 stdout, stderr = Command('/tmp').execute(
137 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
138 assert 'not found' in stderr
139
140 def test_clone_non_existing_store_path_svn(self, rc_web_server, tmpdir, user_util):
141 repo = user_util.create_repo(repo_type='git')
142 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
143
144 # Damage repo by removing it's folder
145 RepoModel()._delete_filesystem_repo(repo)
146
147 username, password = '', ''
148 flags, auth = get_cli_flags(username, password)
149
150 stdout, stderr = Command('/tmp').execute(
151 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
152 assert 'not found' in stderr
153
154 def test_push_new_file_svn(self, rc_web_server, tmpdir):
155 clone_url = rc_web_server.repo_clone_url(SVN_REPO)
156 username, password = '', ''
157 flags, auth = get_cli_flags(username, password)
158
159 stdout, stderr = Command('/tmp').execute(
160 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
161
162 # commit some stuff into this repo
163 stdout, stderr = _add_files_and_push(
164 'svn', tmpdir.strpath, clone_url=clone_url, username=username, password=password)
165
166 _check_proper_svn_push(stdout, stderr)
167
168 def test_push_wrong_credentials_svn(self, rc_web_server, tmpdir):
169 clone_url = rc_web_server.repo_clone_url(SVN_REPO)
170
171 username, password = rc_web_server.repo_clone_credentials()
172 flags, auth = get_cli_flags(username, password)
173
174 stdout, stderr = Command('/tmp').execute(
175 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
176
177 push_url = rc_web_server.repo_clone_url(
178 SVN_REPO, user='bad', passwd='name')
179 stdout, stderr = _add_files_and_push(
180 'svn', tmpdir.strpath, clone_url=push_url, username=username, password=password)
181
182 assert 'fatal: Authentication failed' in stderr
183
184 def test_push_back_to_wrong_url_svn(self, rc_web_server, tmpdir):
185 clone_url = rc_web_server.repo_clone_url(SVN_REPO)
186 username, password = '', ''
187 flags, auth = get_cli_flags(username, password)
188
189 stdout, stderr = Command('/tmp').execute(
190 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
191
192 stdout, stderr = _add_files_and_push(
193 'svn', tmpdir.strpath,
194 clone_url=rc_web_server.repo_clone_url('not-existing'), username=username, password=password)
195
196 assert 'not found' in stderr
197
198 def test_ip_restriction_svn(self, rc_web_server, tmpdir):
199 user_model = UserModel()
200 username, password = '', ''
201 flags, auth = get_cli_flags(username, password)
202
203 try:
204 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
205 Session().commit()
206 time.sleep(2)
207 clone_url = rc_web_server.repo_clone_url(SVN_REPO)
208
209 stdout, stderr = Command('/tmp').execute(
210 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
211 msg = "The requested URL returned error: 403"
212 assert msg in stderr
213 finally:
214 # release IP restrictions
215 for ip in UserIpMap.getAll():
216 UserIpMap.delete(ip.ip_id)
217 Session().commit()
218
219 time.sleep(2)
220
221 cmd = Command('/tmp')
222 stdout, stderr = cmd.execute(f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
223 cmd.assert_returncode_success()
224 _check_proper_clone(stdout, stderr, 'svn')
@@ -1,5 +1,5 b''
1 [bumpversion]
1 [bumpversion]
2 current_version = 5.0.3
2 current_version = 5.1.0
3 message = release: Bump version {current_version} to {new_version}
3 message = release: Bump version {current_version} to {new_version}
4
4
5 [bumpversion:file:rhodecode/VERSION]
5 [bumpversion:file:rhodecode/VERSION]
@@ -37,31 +37,6 b' test-only:'
37 --cov=rhodecode rhodecode
37 --cov=rhodecode rhodecode
38
38
39
39
40 .PHONY: test-only-mysql
41 ## run tests against mysql
42 test-only-mysql:
43 PYTHONHASHSEED=random \
44 py.test -x -vv -r xw -p no:sugar \
45 --cov-report=term-missing --cov-report=html \
46 --ini-config-override='{"app:main": {"sqlalchemy.db1.url": "mysql://root:qweqwe@localhost/rhodecode_test?charset=utf8"}}' \
47 --cov=rhodecode rhodecode
48
49
50 .PHONY: test-only-postgres
51 ## run tests against postgres
52 test-only-postgres:
53 PYTHONHASHSEED=random \
54 py.test -x -vv -r xw -p no:sugar \
55 --cov-report=term-missing --cov-report=html \
56 --ini-config-override='{"app:main": {"sqlalchemy.db1.url": "postgresql://postgres:qweqwe@localhost/rhodecode_test"}}' \
57 --cov=rhodecode rhodecode
58
59 .PHONY: ruff-check
60 ## run a ruff analysis
61 ruff-check:
62 ruff check --ignore F401 --ignore I001 --ignore E402 --ignore E501 --ignore F841 --exclude rhodecode/lib/dbmigrate --exclude .eggs --exclude .dev .
63
64
65 .PHONY: docs
40 .PHONY: docs
66 ## build docs
41 ## build docs
67 docs:
42 docs:
@@ -88,6 +63,10 b' web-build:'
88 ./rhodecode/tests/scripts/static-file-check.sh rhodecode/public/
63 ./rhodecode/tests/scripts/static-file-check.sh rhodecode/public/
89 rm -rf node_modules
64 rm -rf node_modules
90
65
66 .PHONY: ruff-check
67 ## run a ruff analysis
68 ruff-check:
69 ruff check --ignore F401 --ignore I001 --ignore E402 --ignore E501 --ignore F841 --exclude rhodecode/lib/dbmigrate --exclude .eggs --exclude .dev .
91
70
92 .PHONY: pip-packages
71 .PHONY: pip-packages
93 ## Show outdated packages
72 ## Show outdated packages
@@ -109,8 +88,9 b' dev-sh:'
109 sudo apt-get install -y zsh carapace-bin
88 sudo apt-get install -y zsh carapace-bin
110 rm -rf /home/rhodecode/.oh-my-zsh
89 rm -rf /home/rhodecode/.oh-my-zsh
111 curl https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh | sh
90 curl https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh | sh
112 echo "source <(carapace _carapace)" > /home/rhodecode/.zsrc
91 @echo "source <(carapace _carapace)" > /home/rhodecode/.zsrc
113 PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# ' zsh
92 @echo "${RC_DEV_CMD_HELP}"
93 @PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# ' zsh
114
94
115
95
116 .PHONY: dev-cleanup
96 .PHONY: dev-cleanup
@@ -122,7 +102,9 b' dev-cleanup:'
122
102
123 .PHONY: dev-env
103 .PHONY: dev-env
124 ## make dev-env based on the requirements files and install develop of packages
104 ## make dev-env based on the requirements files and install develop of packages
105 ## Cleanup: pip freeze | grep -v "^-e" | grep -v "@" | xargs pip uninstall -y
125 dev-env:
106 dev-env:
107 sudo -u root chown rhodecode:rhodecode /home/rhodecode/.cache/pip/
126 pip install build virtualenv
108 pip install build virtualenv
127 pushd ../rhodecode-vcsserver/ && make dev-env && popd
109 pushd ../rhodecode-vcsserver/ && make dev-env && popd
128 pip wheel --wheel-dir=/home/rhodecode/.cache/pip/wheels -r requirements.txt -r requirements_rc_tools.txt -r requirements_test.txt -r requirements_debug.txt
110 pip wheel --wheel-dir=/home/rhodecode/.cache/pip/wheels -r requirements.txt -r requirements_rc_tools.txt -r requirements_test.txt -r requirements_debug.txt
@@ -137,16 +119,13 b' sh:'
137 make dev-sh
119 make dev-sh
138
120
139
121
140 .PHONY: dev-srv
122 ## Allows changes of workers e.g make dev-srv-g workers=2
141 ## run develop server instance, docker exec -it $(docker ps -q --filter 'name=dev-enterprise-ce') /bin/bash
123 workers?=1
142 dev-srv:
143 pserve --reload .dev/dev.ini
144
124
145
125 .PHONY: dev-srv
146 .PHONY: dev-srv-g
126 ## run gunicorn web server with reloader, use workers=N to set multiworker mode
147 ## run gunicorn multi process workers
127 dev-srv:
148 dev-srv-g:
128 gunicorn --paste=.dev/dev.ini --bind=0.0.0.0:10020 --config=.dev/gunicorn_config.py --timeout=120 --reload --workers=$(workers)
149 gunicorn --paste .dev/dev.ini --bind=0.0.0.0:10020 --config=.dev/gunicorn_config.py --timeout=120 --reload
150
129
151
130
152 # Default command on calling make
131 # Default command on calling make
@@ -31,32 +31,15 b' debug = true'
31 host = 127.0.0.1
31 host = 127.0.0.1
32 port = 10020
32 port = 10020
33
33
34 ; ##################################################
35 ; WAITRESS WSGI SERVER - Recommended for Development
36 ; ##################################################
37
38 ; use server type
39 use = egg:waitress#main
40
41 ; number of worker threads
42 threads = 5
43
44 ; MAX BODY SIZE 100GB
45 max_request_body_size = 107374182400
46
47 ; Use poll instead of select, fixes file descriptors limits problems.
48 ; May not work on old windows systems.
49 asyncore_use_poll = true
50
51
34
52 ; ###########################
35 ; ###########################
53 ; GUNICORN APPLICATION SERVER
36 ; GUNICORN APPLICATION SERVER
54 ; ###########################
37 ; ###########################
55
38
56 ; run with gunicorn --paste rhodecode.ini --config gunicorn_conf.py
39 ; run with gunicorn --config gunicorn_conf.py --paste rhodecode.ini
57
40
58 ; Module to use, this setting shouldn't be changed
41 ; Module to use, this setting shouldn't be changed
59 #use = egg:gunicorn#main
42 use = egg:gunicorn#main
60
43
61 ; Prefix middleware for RhodeCode.
44 ; Prefix middleware for RhodeCode.
62 ; recommended when using proxy setup.
45 ; recommended when using proxy setup.
@@ -153,6 +136,12 b' startup.import_repos = false'
153 ; SSH calls. Set this for events to receive proper url for SSH calls.
136 ; SSH calls. Set this for events to receive proper url for SSH calls.
154 app.base_url = http://rhodecode.local
137 app.base_url = http://rhodecode.local
155
138
139 ; Host at which the Service API is running.
140 app.service_api.host = http://rhodecode.local:10020
141
142 ; Secret for Service API authentication.
143 app.service_api.token =
144
156 ; Unique application ID. Should be a random unique string for security.
145 ; Unique application ID. Should be a random unique string for security.
157 app_instance_uuid = rc-production
146 app_instance_uuid = rc-production
158
147
@@ -255,8 +244,8 b' auth_ret_code_detection = false'
255 ; codes don't break the transactions while 4XX codes do
244 ; codes don't break the transactions while 4XX codes do
256 lock_ret_code = 423
245 lock_ret_code = 423
257
246
258 ; allows to change the repository location in settings page
247 ; Filesystem location were repositories should be stored
259 allow_repo_location_change = true
248 repo_store.path = /var/opt/rhodecode_repo_store
260
249
261 ; allows to setup custom hooks in settings page
250 ; allows to setup custom hooks in settings page
262 allow_custom_hooks_settings = true
251 allow_custom_hooks_settings = true
@@ -298,23 +287,72 b' file_store.enabled = true'
298 ; Storage backend, available options are: local
287 ; Storage backend, available options are: local
299 file_store.backend = local
288 file_store.backend = local
300
289
301 ; path to store the uploaded binaries
290 ; path to store the uploaded binaries and artifacts
302 file_store.storage_path = %(here)s/data/file_store
291 file_store.storage_path = /var/opt/rhodecode_data/file_store
292
293
294 ; Redis url to acquire/check generation of archives locks
295 archive_cache.locking.url = redis://redis:6379/1
296
297 ; Storage backend, only 'filesystem' and 'objectstore' are available now
298 archive_cache.backend.type = filesystem
299
300 ; url for s3 compatible storage that allows to upload artifacts
301 ; e.g http://minio:9000
302 archive_cache.objectstore.url = http://s3-minio:9000
303
304 ; key for s3 auth
305 archive_cache.objectstore.key = key
306
307 ; secret for s3 auth
308 archive_cache.objectstore.secret = secret
303
309
304 ; Uncomment and set this path to control settings for archive download cache.
310 ;region for s3 storage
311 archive_cache.objectstore.region = eu-central-1
312
313 ; number of sharded buckets to create to distribute archives across
314 ; default is 8 shards
315 archive_cache.objectstore.bucket_shards = 8
316
317 ; a top-level bucket to put all other shards in
318 ; objects will be stored in rhodecode-archive-cache/shard-N based on the bucket_shards number
319 archive_cache.objectstore.bucket = rhodecode-archive-cache
320
321 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
322 archive_cache.objectstore.retry = false
323
324 ; number of seconds to wait for next try using retry
325 archive_cache.objectstore.retry_backoff = 1
326
327 ; how many tries do do a retry fetch from this backend
328 archive_cache.objectstore.retry_attempts = 10
329
330 ; Default is $cache_dir/archive_cache if not set
305 ; Generated repo archives will be cached at this location
331 ; Generated repo archives will be cached at this location
306 ; and served from the cache during subsequent requests for the same archive of
332 ; and served from the cache during subsequent requests for the same archive of
307 ; the repository. This path is important to be shared across filesystems and with
333 ; the repository. This path is important to be shared across filesystems and with
308 ; RhodeCode and vcsserver
334 ; RhodeCode and vcsserver
309
335 archive_cache.filesystem.store_dir = /var/opt/rhodecode_data/archive_cache
310 ; Default is $cache_dir/archive_cache if not set
311 archive_cache.store_dir = %(here)s/data/archive_cache
312
336
313 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
337 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
314 archive_cache.cache_size_gb = 10
338 archive_cache.filesystem.cache_size_gb = 1
339
340 ; Eviction policy used to clear out after cache_size_gb limit is reached
341 archive_cache.filesystem.eviction_policy = least-recently-stored
315
342
316 ; By default cache uses sharding technique, this specifies how many shards are there
343 ; By default cache uses sharding technique, this specifies how many shards are there
317 archive_cache.cache_shards = 10
344 ; default is 8 shards
345 archive_cache.filesystem.cache_shards = 8
346
347 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
348 archive_cache.filesystem.retry = false
349
350 ; number of seconds to wait for next try using retry
351 archive_cache.filesystem.retry_backoff = 1
352
353 ; how many tries do do a retry fetch from this backend
354 archive_cache.filesystem.retry_attempts = 10
355
318
356
319 ; #############
357 ; #############
320 ; CELERY CONFIG
358 ; CELERY CONFIG
@@ -322,7 +360,7 b' archive_cache.cache_shards = 10'
322
360
323 ; 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
361 ; 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
324
362
325 use_celery = false
363 use_celery = true
326
364
327 ; path to store schedule database
365 ; path to store schedule database
328 #celerybeat-schedule.path =
366 #celerybeat-schedule.path =
@@ -348,7 +386,7 b' celery.task_always_eager = false'
348
386
349 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
387 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
350 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
388 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
351 cache_dir = %(here)s/data
389 cache_dir = /var/opt/rhodecode_data
352
390
353 ; *********************************************
391 ; *********************************************
354 ; `sql_cache_short` cache for heavy SQL queries
392 ; `sql_cache_short` cache for heavy SQL queries
@@ -457,12 +495,12 b' rc_cache.cache_repo.expiration_time = 25'
457 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
495 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
458 ; types are file, ext:redis, ext:database, ext:memcached
496 ; types are file, ext:redis, ext:database, ext:memcached
459 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
497 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
460 beaker.session.type = file
498 #beaker.session.type = file
461 beaker.session.data_dir = %(here)s/data/sessions
499 #beaker.session.data_dir = %(here)s/data/sessions
462
500
463 ; Redis based sessions
501 ; Redis based sessions
464 #beaker.session.type = ext:redis
502 beaker.session.type = ext:redis
465 #beaker.session.url = redis://127.0.0.1:6379/2
503 beaker.session.url = redis://redis:6379/2
466
504
467 ; DB based session, fast, and allows easy management over logged in users
505 ; DB based session, fast, and allows easy management over logged in users
468 #beaker.session.type = ext:database
506 #beaker.session.type = ext:database
@@ -474,7 +512,7 b' beaker.session.data_dir = %(here)s/data/'
474
512
475 beaker.session.key = rhodecode
513 beaker.session.key = rhodecode
476 beaker.session.secret = develop-rc-uytcxaz
514 beaker.session.secret = develop-rc-uytcxaz
477 beaker.session.lock_dir = %(here)s/data/sessions/lock
515 beaker.session.lock_dir = /data_ramdisk/lock
478
516
479 ; Secure encrypted cookie. Requires AES and AES python libraries
517 ; Secure encrypted cookie. Requires AES and AES python libraries
480 ; you must disable beaker.session.secret to use this
518 ; you must disable beaker.session.secret to use this
@@ -515,18 +553,18 b' search.location = %(here)s/data/index'
515 ; channelstream enables persistent connections and live notification
553 ; channelstream enables persistent connections and live notification
516 ; in the system. It's also used by the chat system
554 ; in the system. It's also used by the chat system
517
555
518 channelstream.enabled = false
556 channelstream.enabled = true
519
557
520 ; server address for channelstream server on the backend
558 ; server address for channelstream server on the backend
521 channelstream.server = 127.0.0.1:9800
559 channelstream.server = channelstream:9800
522
560
523 ; location of the channelstream server from outside world
561 ; location of the channelstream server from outside world
524 ; use ws:// for http or wss:// for https. This address needs to be handled
562 ; use ws:// for http or wss:// for https. This address needs to be handled
525 ; by external HTTP server such as Nginx or Apache
563 ; by external HTTP server such as Nginx or Apache
526 ; see Nginx/Apache configuration examples in our docs
564 ; see Nginx/Apache configuration examples in our docs
527 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
565 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
528 channelstream.secret = secret
566 channelstream.secret = ENV_GENERATED
529 channelstream.history.location = %(here)s/channelstream_history
567 channelstream.history.location = /var/opt/rhodecode_data/channelstream_history
530
568
531 ; Internal application path that Javascript uses to connect into.
569 ; Internal application path that Javascript uses to connect into.
532 ; If you use proxy-prefix the prefix should be added before /_channelstream
570 ; If you use proxy-prefix the prefix should be added before /_channelstream
@@ -572,7 +610,7 b' sqlalchemy.db1.pool_recycle = 3600'
572 ; VCS CONFIG
610 ; VCS CONFIG
573 ; ##########
611 ; ##########
574 vcs.server.enable = true
612 vcs.server.enable = true
575 vcs.server = localhost:9900
613 vcs.server = vcsserver:10010
576
614
577 ; Web server connectivity protocol, responsible for web based VCS operations
615 ; Web server connectivity protocol, responsible for web based VCS operations
578 ; Available protocols are:
616 ; Available protocols are:
@@ -585,6 +623,7 b' vcs.scm_app_implementation = http'
585
623
586 ; Push/Pull operations hooks protocol, available options are:
624 ; Push/Pull operations hooks protocol, available options are:
587 ; `http` - use http-rpc backend (default)
625 ; `http` - use http-rpc backend (default)
626 ; `celery` - use celery based hooks
588 vcs.hooks.protocol = http
627 vcs.hooks.protocol = http
589
628
590 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
629 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
@@ -604,11 +643,6 b' vcs.backends = hg, git, svn'
604 ; Wait this number of seconds before killing connection to the vcsserver
643 ; Wait this number of seconds before killing connection to the vcsserver
605 vcs.connection_timeout = 3600
644 vcs.connection_timeout = 3600
606
645
607 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
608 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
609 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
610 #vcs.svn.compatible_version = 1.8
611
612 ; Cache flag to cache vcsserver remote calls locally
646 ; Cache flag to cache vcsserver remote calls locally
613 ; It uses cache_region `cache_repo`
647 ; It uses cache_region `cache_repo`
614 vcs.methods.cache = true
648 vcs.methods.cache = true
@@ -618,14 +652,29 b' vcs.methods.cache = true'
618 ; Maps RhodeCode repo groups into SVN paths for Apache
652 ; Maps RhodeCode repo groups into SVN paths for Apache
619 ; ####################################################
653 ; ####################################################
620
654
655 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
656 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
657 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
658 #vcs.svn.compatible_version = 1.8
659
660 ; Redis connection settings for svn integrations logic
661 ; This connection string needs to be the same on ce and vcsserver
662 vcs.svn.redis_conn = redis://redis:6379/0
663
664 ; Enable SVN proxy of requests over HTTP
665 vcs.svn.proxy.enabled = true
666
667 ; host to connect to running SVN subsystem
668 vcs.svn.proxy.host = http://svn:8090
669
621 ; Enable or disable the config file generation.
670 ; Enable or disable the config file generation.
622 svn.proxy.generate_config = false
671 svn.proxy.generate_config = true
623
672
624 ; Generate config file with `SVNListParentPath` set to `On`.
673 ; Generate config file with `SVNListParentPath` set to `On`.
625 svn.proxy.list_parent_path = true
674 svn.proxy.list_parent_path = true
626
675
627 ; Set location and file name of generated config file.
676 ; Set location and file name of generated config file.
628 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
677 svn.proxy.config_file_path = /etc/rhodecode/conf/svn/mod_dav_svn.conf
629
678
630 ; alternative mod_dav config template. This needs to be a valid mako template
679 ; alternative mod_dav config template. This needs to be a valid mako template
631 ; Example template can be found in the source code:
680 ; Example template can be found in the source code:
@@ -653,7 +702,7 b' svn.proxy.location_root = /'
653 ; any change user ssh keys. Setting this to false also disables possibility
702 ; any change user ssh keys. Setting this to false also disables possibility
654 ; of adding SSH keys by users from web interface. Super admins can still
703 ; of adding SSH keys by users from web interface. Super admins can still
655 ; manage SSH Keys.
704 ; manage SSH Keys.
656 ssh.generate_authorized_keyfile = false
705 ssh.generate_authorized_keyfile = true
657
706
658 ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
707 ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
659 # ssh.authorized_keys_ssh_opts =
708 # ssh.authorized_keys_ssh_opts =
@@ -661,12 +710,13 b' ssh.generate_authorized_keyfile = false'
661 ; Path to the authorized_keys file where the generate entries are placed.
710 ; Path to the authorized_keys file where the generate entries are placed.
662 ; It is possible to have multiple key files specified in `sshd_config` e.g.
711 ; It is possible to have multiple key files specified in `sshd_config` e.g.
663 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
712 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
664 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
713 ssh.authorized_keys_file_path = /etc/rhodecode/conf/ssh/authorized_keys_rhodecode
665
714
666 ; Command to execute the SSH wrapper. The binary is available in the
715 ; Command to execute the SSH wrapper. The binary is available in the
667 ; RhodeCode installation directory.
716 ; RhodeCode installation directory.
668 ; e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper
717 ; legacy: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
669 ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper
718 ; new rewrite: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
719 ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
670
720
671 ; Allow shell when executing the ssh-wrapper command
721 ; Allow shell when executing the ssh-wrapper command
672 ssh.wrapper_cmd_allow_shell = false
722 ssh.wrapper_cmd_allow_shell = false
@@ -677,73 +727,14 b' ssh.enable_debug_logging = true'
677
727
678 ; Paths to binary executable, by default they are the names, but we can
728 ; Paths to binary executable, by default they are the names, but we can
679 ; override them if we want to use a custom one
729 ; override them if we want to use a custom one
680 ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg
730 ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg
681 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
731 ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git
682 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
732 ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve
683
733
684 ; Enables SSH key generator web interface. Disabling this still allows users
734 ; Enables SSH key generator web interface. Disabling this still allows users
685 ; to add their own keys.
735 ; to add their own keys.
686 ssh.enable_ui_key_generator = true
736 ssh.enable_ui_key_generator = true
687
737
688
689 ; #################
690 ; APPENLIGHT CONFIG
691 ; #################
692
693 ; Appenlight is tailored to work with RhodeCode, see
694 ; http://appenlight.rhodecode.com for details how to obtain an account
695
696 ; Appenlight integration enabled
697 #appenlight = false
698
699 #appenlight.server_url = https://api.appenlight.com
700 #appenlight.api_key = YOUR_API_KEY
701 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
702
703 ; used for JS client
704 #appenlight.api_public_key = YOUR_API_PUBLIC_KEY
705
706 ; TWEAK AMOUNT OF INFO SENT HERE
707
708 ; enables 404 error logging (default False)
709 #appenlight.report_404 = false
710
711 ; time in seconds after request is considered being slow (default 1)
712 #appenlight.slow_request_time = 1
713
714 ; record slow requests in application
715 ; (needs to be enabled for slow datastore recording and time tracking)
716 #appenlight.slow_requests = true
717
718 ; enable hooking to application loggers
719 #appenlight.logging = true
720
721 ; minimum log level for log capture
722 #ppenlight.logging.level = WARNING
723
724 ; send logs only from erroneous/slow requests
725 ; (saves API quota for intensive logging)
726 #appenlight.logging_on_error = false
727
728 ; list of additional keywords that should be grabbed from environ object
729 ; can be string with comma separated list of words in lowercase
730 ; (by default client will always send following info:
731 ; 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
732 ; start with HTTP* this list be extended with additional keywords here
733 #appenlight.environ_keys_whitelist =
734
735 ; list of keywords that should be blanked from request object
736 ; can be string with comma separated list of words in lowercase
737 ; (by default client will always blank keys that contain following words
738 ; 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
739 ; this list be extended with additional keywords set here
740 #appenlight.request_keys_blacklist =
741
742 ; list of namespaces that should be ignores when gathering log entries
743 ; can be string with comma separated list of namespaces
744 ; (by default the client ignores own entries: appenlight_client.client)
745 #appenlight.log_namespace_blacklist =
746
747 ; Statsd client config, this is used to send metrics to statsd
738 ; Statsd client config, this is used to send metrics to statsd
748 ; We recommend setting statsd_exported and scrape them using Prometheus
739 ; We recommend setting statsd_exported and scrape them using Prometheus
749 #statsd.enabled = false
740 #statsd.enabled = false
@@ -36,7 +36,7 b' port = 10020'
36 ; GUNICORN APPLICATION SERVER
36 ; GUNICORN APPLICATION SERVER
37 ; ###########################
37 ; ###########################
38
38
39 ; run with gunicorn --paste rhodecode.ini --config gunicorn_conf.py
39 ; run with gunicorn --config gunicorn_conf.py --paste rhodecode.ini
40
40
41 ; Module to use, this setting shouldn't be changed
41 ; Module to use, this setting shouldn't be changed
42 use = egg:gunicorn#main
42 use = egg:gunicorn#main
@@ -104,6 +104,12 b' startup.import_repos = false'
104 ; SSH calls. Set this for events to receive proper url for SSH calls.
104 ; SSH calls. Set this for events to receive proper url for SSH calls.
105 app.base_url = http://rhodecode.local
105 app.base_url = http://rhodecode.local
106
106
107 ; Host at which the Service API is running.
108 app.service_api.host = http://rhodecode.local:10020
109
110 ; Secret for Service API authentication.
111 app.service_api.token =
112
107 ; Unique application ID. Should be a random unique string for security.
113 ; Unique application ID. Should be a random unique string for security.
108 app_instance_uuid = rc-production
114 app_instance_uuid = rc-production
109
115
@@ -206,8 +212,8 b' auth_ret_code_detection = false'
206 ; codes don't break the transactions while 4XX codes do
212 ; codes don't break the transactions while 4XX codes do
207 lock_ret_code = 423
213 lock_ret_code = 423
208
214
209 ; allows to change the repository location in settings page
215 ; Filesystem location were repositories should be stored
210 allow_repo_location_change = true
216 repo_store.path = /var/opt/rhodecode_repo_store
211
217
212 ; allows to setup custom hooks in settings page
218 ; allows to setup custom hooks in settings page
213 allow_custom_hooks_settings = true
219 allow_custom_hooks_settings = true
@@ -249,23 +255,72 b' file_store.enabled = true'
249 ; Storage backend, available options are: local
255 ; Storage backend, available options are: local
250 file_store.backend = local
256 file_store.backend = local
251
257
252 ; path to store the uploaded binaries
258 ; path to store the uploaded binaries and artifacts
253 file_store.storage_path = %(here)s/data/file_store
259 file_store.storage_path = /var/opt/rhodecode_data/file_store
260
261
262 ; Redis url to acquire/check generation of archives locks
263 archive_cache.locking.url = redis://redis:6379/1
264
265 ; Storage backend, only 'filesystem' and 'objectstore' are available now
266 archive_cache.backend.type = filesystem
267
268 ; url for s3 compatible storage that allows to upload artifacts
269 ; e.g http://minio:9000
270 archive_cache.objectstore.url = http://s3-minio:9000
271
272 ; key for s3 auth
273 archive_cache.objectstore.key = key
274
275 ; secret for s3 auth
276 archive_cache.objectstore.secret = secret
254
277
255 ; Uncomment and set this path to control settings for archive download cache.
278 ;region for s3 storage
279 archive_cache.objectstore.region = eu-central-1
280
281 ; number of sharded buckets to create to distribute archives across
282 ; default is 8 shards
283 archive_cache.objectstore.bucket_shards = 8
284
285 ; a top-level bucket to put all other shards in
286 ; objects will be stored in rhodecode-archive-cache/shard-N based on the bucket_shards number
287 archive_cache.objectstore.bucket = rhodecode-archive-cache
288
289 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
290 archive_cache.objectstore.retry = false
291
292 ; number of seconds to wait for next try using retry
293 archive_cache.objectstore.retry_backoff = 1
294
295 ; how many tries do do a retry fetch from this backend
296 archive_cache.objectstore.retry_attempts = 10
297
298 ; Default is $cache_dir/archive_cache if not set
256 ; Generated repo archives will be cached at this location
299 ; Generated repo archives will be cached at this location
257 ; and served from the cache during subsequent requests for the same archive of
300 ; and served from the cache during subsequent requests for the same archive of
258 ; the repository. This path is important to be shared across filesystems and with
301 ; the repository. This path is important to be shared across filesystems and with
259 ; RhodeCode and vcsserver
302 ; RhodeCode and vcsserver
260
303 archive_cache.filesystem.store_dir = /var/opt/rhodecode_data/archive_cache
261 ; Default is $cache_dir/archive_cache if not set
262 archive_cache.store_dir = %(here)s/data/archive_cache
263
304
264 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
305 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
265 archive_cache.cache_size_gb = 40
306 archive_cache.filesystem.cache_size_gb = 40
307
308 ; Eviction policy used to clear out after cache_size_gb limit is reached
309 archive_cache.filesystem.eviction_policy = least-recently-stored
266
310
267 ; By default cache uses sharding technique, this specifies how many shards are there
311 ; By default cache uses sharding technique, this specifies how many shards are there
268 archive_cache.cache_shards = 4
312 ; default is 8 shards
313 archive_cache.filesystem.cache_shards = 8
314
315 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
316 archive_cache.filesystem.retry = false
317
318 ; number of seconds to wait for next try using retry
319 archive_cache.filesystem.retry_backoff = 1
320
321 ; how many tries do do a retry fetch from this backend
322 archive_cache.filesystem.retry_attempts = 10
323
269
324
270 ; #############
325 ; #############
271 ; CELERY CONFIG
326 ; CELERY CONFIG
@@ -273,7 +328,7 b' archive_cache.cache_shards = 4'
273
328
274 ; 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
329 ; 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
275
330
276 use_celery = false
331 use_celery = true
277
332
278 ; path to store schedule database
333 ; path to store schedule database
279 #celerybeat-schedule.path =
334 #celerybeat-schedule.path =
@@ -299,7 +354,7 b' celery.task_always_eager = false'
299
354
300 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
355 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
301 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
356 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
302 cache_dir = %(here)s/data
357 cache_dir = /var/opt/rhodecode_data
303
358
304 ; *********************************************
359 ; *********************************************
305 ; `sql_cache_short` cache for heavy SQL queries
360 ; `sql_cache_short` cache for heavy SQL queries
@@ -408,12 +463,12 b' rc_cache.cache_repo.expiration_time = 25'
408 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
463 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
409 ; types are file, ext:redis, ext:database, ext:memcached
464 ; types are file, ext:redis, ext:database, ext:memcached
410 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
465 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
411 beaker.session.type = file
466 #beaker.session.type = file
412 beaker.session.data_dir = %(here)s/data/sessions
467 #beaker.session.data_dir = %(here)s/data/sessions
413
468
414 ; Redis based sessions
469 ; Redis based sessions
415 #beaker.session.type = ext:redis
470 beaker.session.type = ext:redis
416 #beaker.session.url = redis://127.0.0.1:6379/2
471 beaker.session.url = redis://redis:6379/2
417
472
418 ; DB based session, fast, and allows easy management over logged in users
473 ; DB based session, fast, and allows easy management over logged in users
419 #beaker.session.type = ext:database
474 #beaker.session.type = ext:database
@@ -425,7 +480,7 b' beaker.session.data_dir = %(here)s/data/'
425
480
426 beaker.session.key = rhodecode
481 beaker.session.key = rhodecode
427 beaker.session.secret = production-rc-uytcxaz
482 beaker.session.secret = production-rc-uytcxaz
428 beaker.session.lock_dir = %(here)s/data/sessions/lock
483 beaker.session.lock_dir = /data_ramdisk/lock
429
484
430 ; Secure encrypted cookie. Requires AES and AES python libraries
485 ; Secure encrypted cookie. Requires AES and AES python libraries
431 ; you must disable beaker.session.secret to use this
486 ; you must disable beaker.session.secret to use this
@@ -466,18 +521,18 b' search.location = %(here)s/data/index'
466 ; channelstream enables persistent connections and live notification
521 ; channelstream enables persistent connections and live notification
467 ; in the system. It's also used by the chat system
522 ; in the system. It's also used by the chat system
468
523
469 channelstream.enabled = false
524 channelstream.enabled = true
470
525
471 ; server address for channelstream server on the backend
526 ; server address for channelstream server on the backend
472 channelstream.server = 127.0.0.1:9800
527 channelstream.server = channelstream:9800
473
528
474 ; location of the channelstream server from outside world
529 ; location of the channelstream server from outside world
475 ; use ws:// for http or wss:// for https. This address needs to be handled
530 ; use ws:// for http or wss:// for https. This address needs to be handled
476 ; by external HTTP server such as Nginx or Apache
531 ; by external HTTP server such as Nginx or Apache
477 ; see Nginx/Apache configuration examples in our docs
532 ; see Nginx/Apache configuration examples in our docs
478 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
533 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
479 channelstream.secret = secret
534 channelstream.secret = ENV_GENERATED
480 channelstream.history.location = %(here)s/channelstream_history
535 channelstream.history.location = /var/opt/rhodecode_data/channelstream_history
481
536
482 ; Internal application path that Javascript uses to connect into.
537 ; Internal application path that Javascript uses to connect into.
483 ; If you use proxy-prefix the prefix should be added before /_channelstream
538 ; If you use proxy-prefix the prefix should be added before /_channelstream
@@ -523,7 +578,7 b' sqlalchemy.db1.pool_recycle = 3600'
523 ; VCS CONFIG
578 ; VCS CONFIG
524 ; ##########
579 ; ##########
525 vcs.server.enable = true
580 vcs.server.enable = true
526 vcs.server = localhost:9900
581 vcs.server = vcsserver:10010
527
582
528 ; Web server connectivity protocol, responsible for web based VCS operations
583 ; Web server connectivity protocol, responsible for web based VCS operations
529 ; Available protocols are:
584 ; Available protocols are:
@@ -536,6 +591,7 b' vcs.scm_app_implementation = http'
536
591
537 ; Push/Pull operations hooks protocol, available options are:
592 ; Push/Pull operations hooks protocol, available options are:
538 ; `http` - use http-rpc backend (default)
593 ; `http` - use http-rpc backend (default)
594 ; `celery` - use celery based hooks
539 vcs.hooks.protocol = http
595 vcs.hooks.protocol = http
540
596
541 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
597 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
@@ -555,11 +611,6 b' vcs.backends = hg, git, svn'
555 ; Wait this number of seconds before killing connection to the vcsserver
611 ; Wait this number of seconds before killing connection to the vcsserver
556 vcs.connection_timeout = 3600
612 vcs.connection_timeout = 3600
557
613
558 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
559 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
560 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
561 #vcs.svn.compatible_version = 1.8
562
563 ; Cache flag to cache vcsserver remote calls locally
614 ; Cache flag to cache vcsserver remote calls locally
564 ; It uses cache_region `cache_repo`
615 ; It uses cache_region `cache_repo`
565 vcs.methods.cache = true
616 vcs.methods.cache = true
@@ -569,14 +620,29 b' vcs.methods.cache = true'
569 ; Maps RhodeCode repo groups into SVN paths for Apache
620 ; Maps RhodeCode repo groups into SVN paths for Apache
570 ; ####################################################
621 ; ####################################################
571
622
623 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
624 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
625 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
626 #vcs.svn.compatible_version = 1.8
627
628 ; Redis connection settings for svn integrations logic
629 ; This connection string needs to be the same on ce and vcsserver
630 vcs.svn.redis_conn = redis://redis:6379/0
631
632 ; Enable SVN proxy of requests over HTTP
633 vcs.svn.proxy.enabled = true
634
635 ; host to connect to running SVN subsystem
636 vcs.svn.proxy.host = http://svn:8090
637
572 ; Enable or disable the config file generation.
638 ; Enable or disable the config file generation.
573 svn.proxy.generate_config = false
639 svn.proxy.generate_config = true
574
640
575 ; Generate config file with `SVNListParentPath` set to `On`.
641 ; Generate config file with `SVNListParentPath` set to `On`.
576 svn.proxy.list_parent_path = true
642 svn.proxy.list_parent_path = true
577
643
578 ; Set location and file name of generated config file.
644 ; Set location and file name of generated config file.
579 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
645 svn.proxy.config_file_path = /etc/rhodecode/conf/svn/mod_dav_svn.conf
580
646
581 ; alternative mod_dav config template. This needs to be a valid mako template
647 ; alternative mod_dav config template. This needs to be a valid mako template
582 ; Example template can be found in the source code:
648 ; Example template can be found in the source code:
@@ -604,7 +670,7 b' svn.proxy.location_root = /'
604 ; any change user ssh keys. Setting this to false also disables possibility
670 ; any change user ssh keys. Setting this to false also disables possibility
605 ; of adding SSH keys by users from web interface. Super admins can still
671 ; of adding SSH keys by users from web interface. Super admins can still
606 ; manage SSH Keys.
672 ; manage SSH Keys.
607 ssh.generate_authorized_keyfile = false
673 ssh.generate_authorized_keyfile = true
608
674
609 ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
675 ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
610 # ssh.authorized_keys_ssh_opts =
676 # ssh.authorized_keys_ssh_opts =
@@ -612,12 +678,13 b' ssh.generate_authorized_keyfile = false'
612 ; Path to the authorized_keys file where the generate entries are placed.
678 ; Path to the authorized_keys file where the generate entries are placed.
613 ; It is possible to have multiple key files specified in `sshd_config` e.g.
679 ; It is possible to have multiple key files specified in `sshd_config` e.g.
614 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
680 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
615 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
681 ssh.authorized_keys_file_path = /etc/rhodecode/conf/ssh/authorized_keys_rhodecode
616
682
617 ; Command to execute the SSH wrapper. The binary is available in the
683 ; Command to execute the SSH wrapper. The binary is available in the
618 ; RhodeCode installation directory.
684 ; RhodeCode installation directory.
619 ; e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper
685 ; legacy: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
620 ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper
686 ; new rewrite: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
687 ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
621
688
622 ; Allow shell when executing the ssh-wrapper command
689 ; Allow shell when executing the ssh-wrapper command
623 ssh.wrapper_cmd_allow_shell = false
690 ssh.wrapper_cmd_allow_shell = false
@@ -628,73 +695,14 b' ssh.enable_debug_logging = false'
628
695
629 ; Paths to binary executable, by default they are the names, but we can
696 ; Paths to binary executable, by default they are the names, but we can
630 ; override them if we want to use a custom one
697 ; override them if we want to use a custom one
631 ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg
698 ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg
632 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
699 ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git
633 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
700 ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve
634
701
635 ; Enables SSH key generator web interface. Disabling this still allows users
702 ; Enables SSH key generator web interface. Disabling this still allows users
636 ; to add their own keys.
703 ; to add their own keys.
637 ssh.enable_ui_key_generator = true
704 ssh.enable_ui_key_generator = true
638
705
639
640 ; #################
641 ; APPENLIGHT CONFIG
642 ; #################
643
644 ; Appenlight is tailored to work with RhodeCode, see
645 ; http://appenlight.rhodecode.com for details how to obtain an account
646
647 ; Appenlight integration enabled
648 #appenlight = false
649
650 #appenlight.server_url = https://api.appenlight.com
651 #appenlight.api_key = YOUR_API_KEY
652 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
653
654 ; used for JS client
655 #appenlight.api_public_key = YOUR_API_PUBLIC_KEY
656
657 ; TWEAK AMOUNT OF INFO SENT HERE
658
659 ; enables 404 error logging (default False)
660 #appenlight.report_404 = false
661
662 ; time in seconds after request is considered being slow (default 1)
663 #appenlight.slow_request_time = 1
664
665 ; record slow requests in application
666 ; (needs to be enabled for slow datastore recording and time tracking)
667 #appenlight.slow_requests = true
668
669 ; enable hooking to application loggers
670 #appenlight.logging = true
671
672 ; minimum log level for log capture
673 #ppenlight.logging.level = WARNING
674
675 ; send logs only from erroneous/slow requests
676 ; (saves API quota for intensive logging)
677 #appenlight.logging_on_error = false
678
679 ; list of additional keywords that should be grabbed from environ object
680 ; can be string with comma separated list of words in lowercase
681 ; (by default client will always send following info:
682 ; 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
683 ; start with HTTP* this list be extended with additional keywords here
684 #appenlight.environ_keys_whitelist =
685
686 ; list of keywords that should be blanked from request object
687 ; can be string with comma separated list of words in lowercase
688 ; (by default client will always blank keys that contain following words
689 ; 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
690 ; this list be extended with additional keywords set here
691 #appenlight.request_keys_blacklist =
692
693 ; list of namespaces that should be ignores when gathering log entries
694 ; can be string with comma separated list of namespaces
695 ; (by default the client ignores own entries: appenlight_client.client)
696 #appenlight.log_namespace_blacklist =
697
698 ; Statsd client config, this is used to send metrics to statsd
706 ; Statsd client config, this is used to send metrics to statsd
699 ; We recommend setting statsd_exported and scrape them using Prometheus
707 ; We recommend setting statsd_exported and scrape them using Prometheus
700 #statsd.enabled = false
708 #statsd.enabled = false
@@ -8,7 +8,7 b' level of support to optimize for product'
8 use the following instructions:
8 use the following instructions:
9
9
10 1. Open the |RCE| configuration file,
10 1. Open the |RCE| configuration file,
11 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
11 :file:`config/_shared/rhodecode.ini`
12
12
13 2. Add the following configuration option in the ``[app:main]`` section.
13 2. Add the following configuration option in the ``[app:main]`` section.
14
14
@@ -42,7 +42,7 b' information see the :ref:`apache-ws-ref`'
42
42
43 |RCE| can also be configured to force strict *https* connections and Strict
43 |RCE| can also be configured to force strict *https* connections and Strict
44 Transport Security. To set this, configure the following options to ``true``
44 Transport Security. To set this, configure the following options to ``true``
45 in the :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
45 in the :file:`config/_shared/rhodecode.ini` file.
46
46
47 .. code-block:: ini
47 .. code-block:: ini
48
48
@@ -83,7 +83,7 b' see the `OpenSSL PKI tutorial`_ site, or'
83
83
84 If the network you are running is SSL/TLS encrypted, you can configure |RCE|
84 If the network you are running is SSL/TLS encrypted, you can configure |RCE|
85 to always use secure connections using the ``force_https`` and ``use_htsts``
85 to always use secure connections using the ``force_https`` and ``use_htsts``
86 options in the :file:`/home/user/.rccontrol/instance-id/rhodecode.ini` file.
86 options in the :file:`config/_shared/rhodecode.ini` file.
87 For more details, see the :ref:`x-frame` section.
87 For more details, see the :ref:`x-frame` section.
88
88
89 FireWalls and Ports
89 FireWalls and Ports
@@ -78,7 +78,7 b' For example:'
78 Configuration Files
78 Configuration Files
79 -------------------
79 -------------------
80
80
81 * :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
81 * :file:`config/_shared/rhodecode.ini`
82 * :file:`/home/{user}/.rccontrol/{instance-id}/search_mapping.ini`
82 * :file:`/home/{user}/.rccontrol/{instance-id}/search_mapping.ini`
83 * :file:`/home/{user}/.rccontrol/{vcsserver-id}/vcsserver.ini`
83 * :file:`/home/{user}/.rccontrol/{vcsserver-id}/vcsserver.ini`
84 * :file:`/home/{user}/.rccontrol/supervisor/supervisord.ini`
84 * :file:`/home/{user}/.rccontrol/supervisor/supervisord.ini`
@@ -188,7 +188,7 b' Changing Default Language'
188 ^^^^^^^^^^^^^^^^^^^^^^^^^
188 ^^^^^^^^^^^^^^^^^^^^^^^^^
189
189
190 To change the default language of a |RCE| instance, change the language code
190 To change the default language of a |RCE| instance, change the language code
191 in the :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. To
191 in the :file:`config/_shared/rhodecode.ini` file. To
192 do this, use the following steps.
192 do this, use the following steps.
193
193
194 1. Open the :file:`rhodecode.ini` file and set the required language code.
194 1. Open the :file:`rhodecode.ini` file and set the required language code.
@@ -11,7 +11,7 b' sections.'
11
11
12 \- **rhodecode.ini**
12 \- **rhodecode.ini**
13 Default location:
13 Default location:
14 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
14 :file:`config/_shared/rhodecode.ini`
15
15
16 This is the main |RCE| configuration file and controls much of its
16 This is the main |RCE| configuration file and controls much of its
17 default behaviour. It is also used to configure certain customer
17 default behaviour. It is also used to configure certain customer
@@ -14,7 +14,7 b' track particular user logs only, and exc'
14 simply grep by `req_id` uuid which you'll have to find for the individual request.
14 simply grep by `req_id` uuid which you'll have to find for the individual request.
15
15
16 To enable debug mode on a |RCE| instance you need to set the debug property
16 To enable debug mode on a |RCE| instance you need to set the debug property
17 in the :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. To
17 in the :file:`config/_shared/rhodecode.ini` file. To
18 do this, use the following steps
18 do this, use the following steps
19
19
20 1. Open the file and set the ``debug`` line to ``true``
20 1. Open the file and set the ``debug`` line to ``true``
@@ -38,7 +38,7 b' Debug and Logging Configuration'
38 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39
39
40 Further debugging and logging settings can also be set in the
40 Further debugging and logging settings can also be set in the
41 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
41 :file:`config/_shared/rhodecode.ini` file.
42
42
43 In the logging section, the various packages that run with |RCE| can have
43 In the logging section, the various packages that run with |RCE| can have
44 different debug levels set. If you want to increase the logging level change
44 different debug levels set. If you want to increase the logging level change
@@ -134,7 +134,7 b' 2. Go to the :menuselection:`Admin --> S'
134 :guilabel:`Subversion HTTP Server URL`.
134 :guilabel:`Subversion HTTP Server URL`.
135
135
136 3. Open the |RCE| configuration file,
136 3. Open the |RCE| configuration file,
137 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
137 :file:`config/_shared/rhodecode.ini`
138
138
139 4. Add the following configuration option in the ``[app:main]``
139 4. Add the following configuration option in the ``[app:main]``
140 section if you don't have it yet.
140 section if you don't have it yet.
@@ -4,7 +4,7 b' Change Default Encoding'
4 -----------------------
4 -----------------------
5
5
6 |RCE| uses ``utf8`` encoding by default. You can change the default encoding
6 |RCE| uses ``utf8`` encoding by default. You can change the default encoding
7 in the :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. To
7 in the :file:`config/_shared/rhodecode.ini` file. To
8 change the default encoding used by |RCE|, set a new value for the
8 change the default encoding used by |RCE|, set a new value for the
9 ``default_encoding``.
9 ``default_encoding``.
10
10
@@ -7,7 +7,7 b' When using external authentication tools'
7 password retry loop in |hg| can result in users being locked out due to too
7 password retry loop in |hg| can result in users being locked out due to too
8 many failed password attempts. To prevent this from happening, add the
8 many failed password attempts. To prevent this from happening, add the
9 following setting to your
9 following setting to your
10 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file, in the
10 :file:`config/_shared/rhodecode.ini` file, in the
11 ``[app:main]`` section.
11 ``[app:main]`` section.
12
12
13
13
@@ -100,7 +100,7 b' Each one should already connect to share'
100
100
101 1) Assuming our final url will be http://rc-node-1, Configure `instances_id`, `app.base_url`
101 1) Assuming our final url will be http://rc-node-1, Configure `instances_id`, `app.base_url`
102
102
103 a) On **rc-node-2** find the following settings and edit :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
103 a) On **rc-node-2** find the following settings and edit :file:`config/_shared/rhodecode.ini`
104
104
105 .. code-block:: ini
105 .. code-block:: ini
106
106
@@ -109,7 +109,7 b' a) On **rc-node-2** find the following s'
109 app.base_url = http://rc-node-1
109 app.base_url = http://rc-node-1
110
110
111
111
112 b) On **rc-node-3** find the following settings and edit :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
112 b) On **rc-node-3** find the following settings and edit :file:`config/_shared/rhodecode.ini`
113
113
114 .. code-block:: ini
114 .. code-block:: ini
115
115
@@ -121,7 +121,7 b' b) On **rc-node-3** find the following s'
121
121
122 2) Configure `User Session` to use a shared database. Example config that should be
122 2) Configure `User Session` to use a shared database. Example config that should be
123 changed on both **rc-node-2** and **rc-node-3** .
123 changed on both **rc-node-2** and **rc-node-3** .
124 Edit :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
124 Edit :file:`config/_shared/rhodecode.ini`
125
125
126 .. code-block:: ini
126 .. code-block:: ini
127
127
@@ -163,7 +163,7 b' 3) Configure stored cached/archive cache'
163
163
164 4) Use shared exception store. Example config that should be
164 4) Use shared exception store. Example config that should be
165 changed on both **rc-node-2** and **rc-node-3**, and also for VCSServer.
165 changed on both **rc-node-2** and **rc-node-3**, and also for VCSServer.
166 Edit :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` and
166 Edit :file:`config/_shared/rhodecode.ini` and
167 :file:`/home/{user}/.rccontrol/{vcsserver-instance-id}/vcsserver.ini`
167 :file:`/home/{user}/.rccontrol/{vcsserver-instance-id}/vcsserver.ini`
168 and add/change following setting.
168 and add/change following setting.
169
169
@@ -15,7 +15,7 b' scalability, and maintainability we reco'
15 sessions to database-based user sessions or Redis based sessions.
15 sessions to database-based user sessions or Redis based sessions.
16
16
17 To switch to database-based user sessions uncomment the following section in
17 To switch to database-based user sessions uncomment the following section in
18 your :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
18 your :file:`config/_shared/rhodecode.ini` file.
19
19
20
20
21 .. code-block:: ini
21 .. code-block:: ini
@@ -49,7 +49,7 b' uses, or if required it can be a differe'
49
49
50
50
51 To switch to redis-based user sessions uncomment the following section in
51 To switch to redis-based user sessions uncomment the following section in
52 your :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
52 your :file:`config/_shared/rhodecode.ini` file.
53
53
54 .. code-block:: ini
54 .. code-block:: ini
55
55
@@ -52,7 +52,7 b' To configure a |RCE| instance to use a V'
52 The following list shows the available options on the |RCE| side of the
52 The following list shows the available options on the |RCE| side of the
53 connection to the VCS Server. The settings are configured per
53 connection to the VCS Server. The settings are configured per
54 instance in the
54 instance in the
55 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
55 :file:`config/_shared/rhodecode.ini` file.
56
56
57 .. rst-class:: dl-horizontal
57 .. rst-class:: dl-horizontal
58
58
@@ -27,7 +27,7 b' of views that have API access enabled by'
27 edit the |RCE| configuration ``.ini`` file. The default location is:
27 edit the |RCE| configuration ``.ini`` file. The default location is:
28
28
29 * |RCE| Pre-2.2.7 :file:`root/rhodecode/data/production.ini`
29 * |RCE| Pre-2.2.7 :file:`root/rhodecode/data/production.ini`
30 * |RCE| 3.0 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
30 * |RCE| 3.0 :file:`config/_shared/rhodecode.ini`
31
31
32 To configure the white list, edit this section of the file. In this
32 To configure the white list, edit this section of the file. In this
33 configuration example, API access is granted to the patch/diff raw file and
33 configuration example, API access is granted to the patch/diff raw file and
@@ -87,7 +87,7 b' 3. Set base_url for instance to enable p'
87 Hostname is required for the integration to properly set the instance URL.
87 Hostname is required for the integration to properly set the instance URL.
88
88
89 When your hostname is known (e.g https://code.rhodecode.com) please set it
89 When your hostname is known (e.g https://code.rhodecode.com) please set it
90 inside :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
90 inside :file:`config/_shared/rhodecode.ini`
91
91
92 add into `[app:main]` section the following configuration:
92 add into `[app:main]` section the following configuration:
93
93
@@ -111,7 +111,7 b' 4. Add the public key to your user accou'
111
111
112 In case of connection problems please set
112 In case of connection problems please set
113 `ssh.enable_debug_logging = true` inside the SSH configuration of
113 `ssh.enable_debug_logging = true` inside the SSH configuration of
114 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
114 :file:`config/_shared/rhodecode.ini`
115 Then add, remove your SSH key and try connecting again.
115 Then add, remove your SSH key and try connecting again.
116 Debug logging will be printed to help find the problems on the server side.
116 Debug logging will be printed to help find the problems on the server side.
117
117
@@ -4,7 +4,7 b' Set up Email'
4 ------------
4 ------------
5
5
6 To setup email with your |RCE| instance, open the default
6 To setup email with your |RCE| instance, open the default
7 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
7 :file:`config/_shared/rhodecode.ini`
8 file and uncomment and configure the email section. If it is not there,
8 file and uncomment and configure the email section. If it is not there,
9 use the below example to insert it.
9 use the below example to insert it.
10
10
@@ -4,7 +4,8 b''
4 Release Date
4 Release Date
5 ^^^^^^^^^^^^
5 ^^^^^^^^^^^^
6
6
7 - TBA
7
8 - 2024-05-14
8
9
9
10
10 New Features
11 New Features
@@ -12,21 +13,20 b' New Features'
12
13
13 - Full support of Python3 and Python3.11
14 - Full support of Python3 and Python3.11
14 - Git repositories with LFS object are now pushing and pulling the LFS objects when remote sync is enabled.
15 - Git repositories with LFS object are now pushing and pulling the LFS objects when remote sync is enabled.
15 - Archive generation: implemented a new system for caching generated archive files that allows setting cache size limit
16 - Archive generation: implemented a new system for caching generated archive files that allows setting cache size limit see: archive_cache.cache_size_gb= option.
16 see: `archive_cache.cache_size_gb=` option.
17 - Introduced statsd metrics in various places for new monitoring stack to provide useful details on traffic and usage.
17 - Introduced statsd metrics in various places for new monitoring stack to provide useful details on traffic and usage.
18
18
19
19
20 General
20 General
21 ^^^^^^^
21 ^^^^^^^
22
22
23 - Upgraded all dependency libraries to their latest available versions
23 - Upgraded all dependency libraries to their latest available versions for python3 compatability
24 - Dropped support for deprecated hgsubversion no longer available in python3
25
24
26
25
27 Security
26 Security
28 ^^^^^^^^
27 ^^^^^^^^
29
28
29 - fixed few edge cases of permission invalidation on change of permissions
30
30
31
31
32 Performance
32 Performance
@@ -38,6 +38,7 b' Performance'
38 Fixes
38 Fixes
39 ^^^^^
39 ^^^^^
40
40
41 - Various small fixes and improvements found during python3 migration
41
42
42
43
43 Upgrade notes
44 Upgrade notes
@@ -9,6 +9,11 b' Release Notes'
9 .. toctree::
9 .. toctree::
10 :maxdepth: 1
10 :maxdepth: 1
11
11
12
13 release-notes-5.1.0.rst
14 release-notes-5.0.3.rst
15 release-notes-5.0.2.rst
16 release-notes-5.0.1.rst
12 release-notes-5.0.0.rst
17 release-notes-5.0.0.rst
13
18
14
19
@@ -27,7 +27,7 b' 1. Install a new instance of |RCE|, choo'
27
27
28 Once the new instance is installed you need to update the licence token and
28 Once the new instance is installed you need to update the licence token and
29 database connection string in the
29 database connection string in the
30 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
30 :file:`config/_shared/rhodecode.ini` file.
31
31
32 .. code-block:: bash
32 .. code-block:: bash
33
33
@@ -47,7 +47,6 b''
47 "moment": "^2.18.1",
47 "moment": "^2.18.1",
48 "mousetrap": "^1.6.1",
48 "mousetrap": "^1.6.1",
49 "polymer-webpack-loader": "^2.0.1",
49 "polymer-webpack-loader": "^2.0.1",
50 "qrious": "^4.0.2",
51 "raw-loader": "1.0.0-beta.0",
50 "raw-loader": "1.0.0-beta.0",
52 "sticky-sidebar": "3.3.1",
51 "sticky-sidebar": "3.3.1",
53 "style-loader": "^0.21.0",
52 "style-loader": "^0.21.0",
@@ -21,3 +21,7 b' markers ='
21 skip_backends: Mark tests as skipped for given backends.
21 skip_backends: Mark tests as skipped for given backends.
22 backends: Mark backends
22 backends: Mark backends
23 dbs: database markers for running tests for given DB
23 dbs: database markers for running tests for given DB
24
25 env =
26 RC_TEST=1
27 RUN_ENV=test
@@ -1,9 +1,9 b''
1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
2
2
3 alembic==1.12.1
3 alembic==1.13.1
4 mako==1.2.4
4 mako==1.2.4
5 markupsafe==2.1.2
5 markupsafe==2.1.2
6 sqlalchemy==1.4.51
6 sqlalchemy==1.4.52
7 greenlet==3.0.3
7 greenlet==3.0.3
8 typing_extensions==4.9.0
8 typing_extensions==4.9.0
9 async-timeout==4.0.3
9 async-timeout==4.0.3
@@ -27,13 +27,13 b' celery==5.3.6'
27 vine==5.1.0
27 vine==5.1.0
28 python-dateutil==2.8.2
28 python-dateutil==2.8.2
29 six==1.16.0
29 six==1.16.0
30 tzdata==2023.4
30 tzdata==2024.1
31 vine==5.1.0
31 vine==5.1.0
32 channelstream==0.7.1
32 channelstream==0.7.1
33 gevent==24.2.1
33 gevent==24.2.1
34 greenlet==3.0.3
34 greenlet==3.0.3
35 zope.event==5.0.0
35 zope.event==5.0.0
36 zope.interface==6.1.0
36 zope.interface==6.3.0
37 itsdangerous==1.1.0
37 itsdangerous==1.1.0
38 marshmallow==2.18.0
38 marshmallow==2.18.0
39 pyramid==2.0.2
39 pyramid==2.0.2
@@ -46,9 +46,7 b' channelstream==0.7.1'
46 venusian==3.0.0
46 venusian==3.0.0
47 webob==1.8.7
47 webob==1.8.7
48 zope.deprecation==5.0.0
48 zope.deprecation==5.0.0
49 zope.interface==6.1.0
49 zope.interface==6.3.0
50 pyramid-apispec==0.3.3
51 apispec==1.3.3
52 pyramid-jinja2==2.10
50 pyramid-jinja2==2.10
53 jinja2==3.1.2
51 jinja2==3.1.2
54 markupsafe==2.1.2
52 markupsafe==2.1.2
@@ -63,7 +61,7 b' channelstream==0.7.1'
63 venusian==3.0.0
61 venusian==3.0.0
64 webob==1.8.7
62 webob==1.8.7
65 zope.deprecation==5.0.0
63 zope.deprecation==5.0.0
66 zope.interface==6.1.0
64 zope.interface==6.3.0
67 zope.deprecation==5.0.0
65 zope.deprecation==5.0.0
68 python-dateutil==2.8.2
66 python-dateutil==2.8.2
69 six==1.16.0
67 six==1.16.0
@@ -82,20 +80,20 b' deform==2.0.15'
82 peppercorn==0.6
80 peppercorn==0.6
83 translationstring==1.4
81 translationstring==1.4
84 zope.deprecation==5.0.0
82 zope.deprecation==5.0.0
85 diskcache==5.6.3
86 docutils==0.19
83 docutils==0.19
87 dogpile.cache==1.3.0
84 dogpile.cache==1.3.3
88 decorator==5.1.1
85 decorator==5.1.1
89 stevedore==5.1.0
86 stevedore==5.1.0
90 pbr==5.11.1
87 pbr==5.11.1
91 formencode==2.1.0
88 formencode==2.1.0
92 six==1.16.0
89 six==1.16.0
90 fsspec==2024.6.0
93 gunicorn==21.2.0
91 gunicorn==21.2.0
94 packaging==23.1
92 packaging==24.0
95 gevent==24.2.1
93 gevent==24.2.1
96 greenlet==3.0.3
94 greenlet==3.0.3
97 zope.event==5.0.0
95 zope.event==5.0.0
98 zope.interface==6.1.0
96 zope.interface==6.3.0
99 ipython==8.14.0
97 ipython==8.14.0
100 backcall==0.2.0
98 backcall==0.2.0
101 decorator==5.1.1
99 decorator==5.1.1
@@ -116,11 +114,11 b' ipython==8.14.0'
116 pure-eval==0.2.2
114 pure-eval==0.2.2
117 traitlets==5.9.0
115 traitlets==5.9.0
118 markdown==3.4.3
116 markdown==3.4.3
119 msgpack==1.0.7
117 msgpack==1.0.8
120 mysqlclient==2.1.1
118 mysqlclient==2.1.1
121 nbconvert==7.7.3
119 nbconvert==7.7.3
122 beautifulsoup4==4.11.2
120 beautifulsoup4==4.12.3
123 soupsieve==2.4
121 soupsieve==2.5
124 bleach==6.1.0
122 bleach==6.1.0
125 six==1.16.0
123 six==1.16.0
126 webencodings==0.5.1
124 webencodings==0.5.1
@@ -165,20 +163,15 b' nbconvert==7.7.3'
165 platformdirs==3.10.0
163 platformdirs==3.10.0
166 traitlets==5.9.0
164 traitlets==5.9.0
167 traitlets==5.9.0
165 traitlets==5.9.0
168 packaging==23.1
169 pandocfilters==1.5.0
166 pandocfilters==1.5.0
170 pygments==2.15.1
167 pygments==2.15.1
171 tinycss2==1.2.1
168 tinycss2==1.2.1
172 webencodings==0.5.1
169 webencodings==0.5.1
173 traitlets==5.9.0
170 traitlets==5.9.0
174 orjson==3.9.13
171 orjson==3.10.3
175 pastescript==3.4.0
172 paste==3.10.1
176 paste==3.7.1
177 six==1.16.0
178 pastedeploy==3.1.0
179 six==1.16.0
180 premailer==3.10.0
173 premailer==3.10.0
181 cachetools==5.3.2
174 cachetools==5.3.3
182 cssselect==1.2.0
175 cssselect==1.2.0
183 cssutils==2.6.0
176 cssutils==2.6.0
184 lxml==4.9.3
177 lxml==4.9.3
@@ -194,11 +187,11 b' pycmarkgfm==1.2.0'
194 cffi==1.16.0
187 cffi==1.16.0
195 pycparser==2.21
188 pycparser==2.21
196 pycryptodome==3.17
189 pycryptodome==3.17
197 pycurl==7.45.2
190 pycurl==7.45.3
198 pymysql==1.0.3
191 pymysql==1.0.3
199 pyotp==2.8.0
192 pyotp==2.8.0
200 pyparsing==3.1.1
193 pyparsing==3.1.1
201 pyramid-debugtoolbar==4.11
194 pyramid-debugtoolbar==4.12.1
202 pygments==2.15.1
195 pygments==2.15.1
203 pyramid==2.0.2
196 pyramid==2.0.2
204 hupper==1.12
197 hupper==1.12
@@ -210,7 +203,7 b' pyramid-debugtoolbar==4.11'
210 venusian==3.0.0
203 venusian==3.0.0
211 webob==1.8.7
204 webob==1.8.7
212 zope.deprecation==5.0.0
205 zope.deprecation==5.0.0
213 zope.interface==6.1.0
206 zope.interface==6.3.0
214 pyramid-mako==1.1.0
207 pyramid-mako==1.1.0
215 mako==1.2.4
208 mako==1.2.4
216 markupsafe==2.1.2
209 markupsafe==2.1.2
@@ -224,7 +217,7 b' pyramid-debugtoolbar==4.11'
224 venusian==3.0.0
217 venusian==3.0.0
225 webob==1.8.7
218 webob==1.8.7
226 zope.deprecation==5.0.0
219 zope.deprecation==5.0.0
227 zope.interface==6.1.0
220 zope.interface==6.3.0
228 pyramid-mailer==0.15.1
221 pyramid-mailer==0.15.1
229 pyramid==2.0.2
222 pyramid==2.0.2
230 hupper==1.12
223 hupper==1.12
@@ -236,13 +229,13 b' pyramid-mailer==0.15.1'
236 venusian==3.0.0
229 venusian==3.0.0
237 webob==1.8.7
230 webob==1.8.7
238 zope.deprecation==5.0.0
231 zope.deprecation==5.0.0
239 zope.interface==6.1.0
232 zope.interface==6.3.0
240 repoze.sendmail==4.4.1
233 repoze.sendmail==4.4.1
241 transaction==3.1.0
234 transaction==3.1.0
242 zope.interface==6.1.0
235 zope.interface==6.3.0
243 zope.interface==6.1.0
236 zope.interface==6.3.0
244 transaction==3.1.0
237 transaction==3.1.0
245 zope.interface==6.1.0
238 zope.interface==6.3.0
246 python-ldap==3.4.3
239 python-ldap==3.4.3
247 pyasn1==0.4.8
240 pyasn1==0.4.8
248 pyasn1-modules==0.2.8
241 pyasn1-modules==0.2.8
@@ -257,39 +250,64 b' python3-saml==1.15.0'
257 xmlsec==1.3.13
250 xmlsec==1.3.13
258 lxml==4.9.3
251 lxml==4.9.3
259 pyyaml==6.0.1
252 pyyaml==6.0.1
260 redis==5.0.1
253 redis==5.0.4
254 async-timeout==4.0.3
261 regex==2022.10.31
255 regex==2022.10.31
262 routes==2.5.1
256 routes==2.5.1
263 repoze.lru==0.7
257 repoze.lru==0.7
264 six==1.16.0
258 six==1.16.0
265 simplejson==3.19.1
259 s3fs==2024.6.0
260 aiobotocore==2.13.0
261 aiohttp==3.9.5
262 aiosignal==1.3.1
263 frozenlist==1.4.1
264 attrs==22.2.0
265 frozenlist==1.4.1
266 multidict==6.0.5
267 yarl==1.9.4
268 idna==3.4
269 multidict==6.0.5
270 aioitertools==0.11.0
271 botocore==1.34.106
272 jmespath==1.0.1
273 python-dateutil==2.8.2
274 six==1.16.0
275 urllib3==1.26.14
276 wrapt==1.16.0
277 aiohttp==3.9.5
278 aiosignal==1.3.1
279 frozenlist==1.4.1
280 attrs==22.2.0
281 frozenlist==1.4.1
282 multidict==6.0.5
283 yarl==1.9.4
284 idna==3.4
285 multidict==6.0.5
286 fsspec==2024.6.0
287 simplejson==3.19.2
266 sshpubkeys==3.3.1
288 sshpubkeys==3.3.1
267 cryptography==40.0.2
289 cryptography==40.0.2
268 cffi==1.16.0
290 cffi==1.16.0
269 pycparser==2.21
291 pycparser==2.21
270 ecdsa==0.18.0
292 ecdsa==0.18.0
271 six==1.16.0
293 six==1.16.0
272 sqlalchemy==1.4.51
294 sqlalchemy==1.4.52
273 greenlet==3.0.3
295 greenlet==3.0.3
274 typing_extensions==4.9.0
296 typing_extensions==4.9.0
275 supervisor==4.2.5
297 supervisor==4.2.5
276 tzlocal==4.3
298 tzlocal==4.3
277 pytz-deprecation-shim==0.1.0.post0
299 pytz-deprecation-shim==0.1.0.post0
278 tzdata==2023.4
300 tzdata==2024.1
301 tempita==0.5.2
279 unidecode==1.3.6
302 unidecode==1.3.6
280 urlobject==2.4.3
303 urlobject==2.4.3
281 waitress==3.0.0
304 waitress==3.0.0
282 weberror==0.13.1
305 webhelpers2==2.1
283 paste==3.7.1
284 six==1.16.0
285 pygments==2.15.1
286 tempita==0.5.2
287 webob==1.8.7
288 webhelpers2==2.0
289 markupsafe==2.1.2
306 markupsafe==2.1.2
290 six==1.16.0
307 six==1.16.0
291 whoosh==2.7.4
308 whoosh==2.7.4
292 zope.cachedescriptors==5.0.0
309 zope.cachedescriptors==5.0.0
310 qrcode==7.4.2
293
311
294 ## uncomment to add the debug libraries
312 ## uncomment to add the debug libraries
295 #-r requirements_debug.txt
313 #-r requirements_debug.txt
@@ -1,43 +1,45 b''
1 # test related requirements
1 # test related requirements
2
2 mock==5.1.0
3 cov-core==1.15.0
3 pytest-cov==4.1.0
4 coverage==7.2.3
4 coverage==7.4.3
5 mock==5.0.2
5 pytest==8.1.1
6 py==1.11.0
7 pytest-cov==4.0.0
8 coverage==7.2.3
9 pytest==7.3.1
10 attrs==22.2.0
11 iniconfig==2.0.0
6 iniconfig==2.0.0
12 packaging==23.1
7 packaging==24.0
13 pluggy==1.0.0
8 pluggy==1.4.0
14 pytest-rerunfailures==12.0
9 pytest-env==1.1.3
10 pytest==8.1.1
11 iniconfig==2.0.0
12 packaging==24.0
13 pluggy==1.4.0
15 pytest-profiling==1.7.0
14 pytest-profiling==1.7.0
16 gprof2dot==2022.7.29
15 gprof2dot==2022.7.29
17 pytest==7.3.1
16 pytest==8.1.1
18 attrs==22.2.0
19 iniconfig==2.0.0
17 iniconfig==2.0.0
20 packaging==23.1
18 packaging==24.0
21 pluggy==1.0.0
19 pluggy==1.4.0
22 six==1.16.0
20 six==1.16.0
23 pytest-runner==6.0.0
21 pytest-rerunfailures==13.0
24 pytest-sugar==0.9.7
22 packaging==24.0
25 packaging==23.1
23 pytest==8.1.1
26 pytest==7.3.1
27 attrs==22.2.0
28 iniconfig==2.0.0
24 iniconfig==2.0.0
29 packaging==23.1
25 packaging==24.0
30 pluggy==1.0.0
26 pluggy==1.4.0
31 termcolor==2.3.0
27 pytest-runner==6.0.1
32 pytest-timeout==2.1.0
28 pytest-sugar==1.0.0
33 pytest==7.3.1
29 packaging==24.0
34 attrs==22.2.0
30 pytest==8.1.1
35 iniconfig==2.0.0
31 iniconfig==2.0.0
36 packaging==23.1
32 packaging==24.0
37 pluggy==1.0.0
33 pluggy==1.4.0
34 termcolor==2.4.0
35 pytest-timeout==2.3.1
36 pytest==8.1.1
37 iniconfig==2.0.0
38 packaging==24.0
39 pluggy==1.4.0
38 webtest==3.0.0
40 webtest==3.0.0
39 beautifulsoup4==4.11.2
41 beautifulsoup4==4.12.3
40 soupsieve==2.4
42 soupsieve==2.5
41 waitress==3.0.0
43 waitress==3.0.0
42 webob==1.8.7
44 webob==1.8.7
43
45
@@ -1,1 +1,1 b''
1 5.0.3 No newline at end of file
1 5.1.0 No newline at end of file
@@ -82,10 +82,10 b' PYRAMID_SETTINGS = {}'
82 EXTENSIONS = {}
82 EXTENSIONS = {}
83
83
84 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
84 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
85 __dbversion__ = 114 # defines current db version for migrations
85 __dbversion__ = 115 # defines current db version for migrations
86 __license__ = 'AGPLv3, and Commercial License'
86 __license__ = 'AGPLv3, and Commercial License'
87 __author__ = 'RhodeCode GmbH'
87 __author__ = 'RhodeCode GmbH'
88 __url__ = 'https://code.rhodecode.com'
88 __url__ = 'https://code.rhodecode.com'
89
89
90 is_test = False
90 is_test = os.getenv('RC_TEST', '0') == '1'
91 disable_error_handler = False
91 disable_error_handler = False
@@ -22,7 +22,6 b' import sys'
22 import fnmatch
22 import fnmatch
23
23
24 import decorator
24 import decorator
25 import typing
26 import venusian
25 import venusian
27 from collections import OrderedDict
26 from collections import OrderedDict
28
27
@@ -45,7 +44,8 b' from rhodecode.model.db import User, Use'
45 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
46
45
47 DEFAULT_RENDERER = 'jsonrpc_renderer'
46 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 DEFAULT_URL = '/_admin/apiv2'
47 DEFAULT_URL = '/_admin/api'
48 SERVICE_API_IDENTIFIER = 'service_'
49
49
50
50
51 def find_methods(jsonrpc_methods, pattern):
51 def find_methods(jsonrpc_methods, pattern):
@@ -54,7 +54,9 b' def find_methods(jsonrpc_methods, patter'
54 pattern = [pattern]
54 pattern = [pattern]
55
55
56 for single_pattern in pattern:
56 for single_pattern in pattern:
57 for method_name, method in jsonrpc_methods.items():
57 for method_name, method in filter(
58 lambda x: not x[0].startswith(SERVICE_API_IDENTIFIER), jsonrpc_methods.items()
59 ):
58 if fnmatch.fnmatch(method_name, single_pattern):
60 if fnmatch.fnmatch(method_name, single_pattern):
59 matches[method_name] = method
61 matches[method_name] = method
60 return matches
62 return matches
@@ -190,6 +192,7 b' def request_view(request):'
190 # check if we can find this session using api_key, get_by_auth_token
192 # check if we can find this session using api_key, get_by_auth_token
191 # search not expired tokens only
193 # search not expired tokens only
192 try:
194 try:
195 if not request.rpc_method.startswith(SERVICE_API_IDENTIFIER):
193 api_user = User.get_by_auth_token(request.rpc_api_key)
196 api_user = User.get_by_auth_token(request.rpc_api_key)
194
197
195 if api_user is None:
198 if api_user is None:
@@ -227,6 +230,10 b' def request_view(request):'
227 return jsonrpc_error(
230 return jsonrpc_error(
228 request, retid=request.rpc_id,
231 request, retid=request.rpc_id,
229 message='API KEY invalid or, has bad role for an API call')
232 message='API KEY invalid or, has bad role for an API call')
233 else:
234 auth_u = 'service'
235 if request.rpc_api_key != request.registry.settings['app.service_api.token']:
236 raise Exception("Provided service secret is not recognized!")
230
237
231 except Exception:
238 except Exception:
232 log.exception('Error on API AUTH')
239 log.exception('Error on API AUTH')
@@ -290,7 +297,8 b' def request_view(request):'
290 })
297 })
291
298
292 # register some common functions for usage
299 # register some common functions for usage
293 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
300 rpc_user = request.rpc_user.user_id if hasattr(request, 'rpc_user') else None
301 attach_context_attributes(TemplateArgs(), request, rpc_user)
294
302
295 statsd = request.registry.statsd
303 statsd = request.registry.statsd
296
304
@@ -41,7 +41,7 b' class TestCreateRepo(object):'
41 @pytest.mark.parametrize('given, expected_name, expected_exc', [
41 @pytest.mark.parametrize('given, expected_name, expected_exc', [
42 ('api repo-1', 'api-repo-1', False),
42 ('api repo-1', 'api-repo-1', False),
43 ('api-repo 1-ąć', 'api-repo-1-ąć', False),
43 ('api-repo 1-ąć', 'api-repo-1-ąć', False),
44 (u'unicode-ąć', u'unicode-ąć', False),
44 ('unicode-ąć', u'unicode-ąć', False),
45 ('some repo v1.2', 'some-repo-v1.2', False),
45 ('some repo v1.2', 'some-repo-v1.2', False),
46 ('v2.0', 'v2.0', False),
46 ('v2.0', 'v2.0', False),
47 ])
47 ])
@@ -211,8 +211,8 b' class TestCreateRepoGroup(object):'
211
211
212 expected = {
212 expected = {
213 'repo_group':
213 'repo_group':
214 u'You do not have the permission to store '
214 'You do not have the permission to store '
215 u'repository groups in the root location.'}
215 'repository groups in the root location.'}
216 assert_error(id_, expected, given=response.body)
216 assert_error(id_, expected, given=response.body)
217
217
218 def test_api_create_repo_group_regular_user_no_parent_group_perms(self):
218 def test_api_create_repo_group_regular_user_no_parent_group_perms(self):
@@ -232,8 +232,8 b' class TestCreateRepoGroup(object):'
232
232
233 expected = {
233 expected = {
234 'repo_group':
234 'repo_group':
235 u"You do not have the permissions to store "
235 "You do not have the permissions to store "
236 u"repository groups inside repository group `{}`".format(repo_group_name)}
236 "repository groups inside repository group `{}`".format(repo_group_name)}
237 try:
237 try:
238 assert_error(id_, expected, given=response.body)
238 assert_error(id_, expected, given=response.body)
239 finally:
239 finally:
@@ -76,8 +76,8 b' class TestApiGetGist(object):'
76 'url': 'http://%s/_admin/gists/%s' % (http_host_only_stub, gist_id,),
76 'url': 'http://%s/_admin/gists/%s' % (http_host_only_stub, gist_id,),
77 'acl_level': Gist.ACL_LEVEL_PUBLIC,
77 'acl_level': Gist.ACL_LEVEL_PUBLIC,
78 'content': {
78 'content': {
79 u'filename1.txt': u'hello world',
79 'filename1.txt': 'hello world',
80 u'filename1Ä….txt': u'hello worldÄ™'
80 'filename1Ä….txt': 'hello worldÄ™'
81 },
81 },
82 }
82 }
83
83
@@ -25,7 +25,7 b' from rhodecode.api import ('
25
25
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 Optional, OAttr, has_superadmin_permission, get_user_or_error)
27 Optional, OAttr, has_superadmin_permission, get_user_or_error)
28 from rhodecode.lib.utils import repo2db_mapper
28 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path
29 from rhodecode.lib import system_info
29 from rhodecode.lib import system_info
30 from rhodecode.lib import user_sessions
30 from rhodecode.lib import user_sessions
31 from rhodecode.lib import exc_tracking
31 from rhodecode.lib import exc_tracking
@@ -33,7 +33,6 b' from rhodecode.lib.ext_json import json'
33 from rhodecode.lib.utils2 import safe_int
33 from rhodecode.lib.utils2 import safe_int
34 from rhodecode.model.db import UserIpMap
34 from rhodecode.model.db import UserIpMap
35 from rhodecode.model.scm import ScmModel
35 from rhodecode.model.scm import ScmModel
36 from rhodecode.model.settings import VcsSettingsModel
37 from rhodecode.apps.file_store import utils
36 from rhodecode.apps.file_store import utils
38 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
37 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
39 FileOverSizeException
38 FileOverSizeException
@@ -103,7 +102,7 b' def get_repo_store(request, apiuser):'
103 if not has_superadmin_permission(apiuser):
102 if not has_superadmin_permission(apiuser):
104 raise JSONRPCForbidden()
103 raise JSONRPCForbidden()
105
104
106 path = VcsSettingsModel().get_repos_location()
105 path = get_rhodecode_repo_store_path()
107 return {"path": path}
106 return {"path": path}
108
107
109
108
@@ -104,6 +104,11 b' class TemplateArgs(StrictAttributeDict):'
104
104
105
105
106 class BaseAppView(object):
106 class BaseAppView(object):
107 DONT_CHECKOUT_VIEWS = ["channelstream_connect", "ops_ping"]
108 EXTRA_VIEWS_TO_IGNORE = ['login', 'register', 'logout']
109 SETUP_2FA_VIEW = 'setup_2fa'
110 VERIFY_2FA_VIEW = 'check_2fa'
111
107 def __init__(self, context, request):
112 def __init__(self, context, request):
108 self.request = request
113 self.request = request
109 self.context = context
114 self.context = context
@@ -117,13 +122,19 b' class BaseAppView(object):'
117
122
118 self._rhodecode_user = request.user # auth user
123 self._rhodecode_user = request.user # auth user
119 self._rhodecode_db_user = self._rhodecode_user.get_instance()
124 self._rhodecode_db_user = self._rhodecode_user.get_instance()
125 self.user_data = self._rhodecode_db_user.user_data if self._rhodecode_db_user else {}
120 self._maybe_needs_password_change(
126 self._maybe_needs_password_change(
121 request.matched_route.name, self._rhodecode_db_user
127 request.matched_route.name, self._rhodecode_db_user
122 )
128 )
129 self._maybe_needs_2fa_configuration(
130 request.matched_route.name, self._rhodecode_db_user
131 )
132 self._maybe_needs_2fa_check(
133 request.matched_route.name, self._rhodecode_db_user
134 )
123
135
124 def _maybe_needs_password_change(self, view_name, user_obj):
136 def _maybe_needs_password_change(self, view_name, user_obj):
125 dont_check_views = ["channelstream_connect", "ops_ping"]
137 if view_name in self.DONT_CHECKOUT_VIEWS:
126 if view_name in dont_check_views:
127 return
138 return
128
139
129 log.debug(
140 log.debug(
@@ -133,6 +144,7 b' class BaseAppView(object):'
133 skip_user_views = [
144 skip_user_views = [
134 "logout",
145 "logout",
135 "login",
146 "login",
147 "check_2fa",
136 "my_account_password",
148 "my_account_password",
137 "my_account_password_update",
149 "my_account_password_update",
138 ]
150 ]
@@ -144,7 +156,7 b' class BaseAppView(object):'
144 return
156 return
145
157
146 now = time.time()
158 now = time.time()
147 should_change = user_obj.user_data.get("force_password_change")
159 should_change = self.user_data.get("force_password_change")
148 change_after = safe_int(should_change) or 0
160 change_after = safe_int(should_change) or 0
149 if should_change and now > change_after:
161 if should_change and now > change_after:
150 log.debug("User %s requires password change", user_obj)
162 log.debug("User %s requires password change", user_obj)
@@ -157,6 +169,33 b' class BaseAppView(object):'
157 if view_name not in skip_user_views:
169 if view_name not in skip_user_views:
158 raise HTTPFound(self.request.route_path("my_account_password"))
170 raise HTTPFound(self.request.route_path("my_account_password"))
159
171
172 def _maybe_needs_2fa_configuration(self, view_name, user_obj):
173 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
174 return
175
176 if not user_obj:
177 return
178
179 if user_obj.needs_2fa_configure and view_name != self.SETUP_2FA_VIEW:
180 h.flash(
181 "You are required to configure 2FA",
182 "warning",
183 ignore_duplicate=False,
184 )
185 # Special case for users created "on the fly" (ldap case for new user)
186 user_obj.check_2fa_required = False
187 raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW))
188
189 def _maybe_needs_2fa_check(self, view_name, user_obj):
190 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
191 return
192
193 if not user_obj:
194 return
195
196 if user_obj.check_2fa_required and view_name != self.VERIFY_2FA_VIEW:
197 raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW))
198
160 def _log_creation_exception(self, e, repo_name):
199 def _log_creation_exception(self, e, repo_name):
161 _ = self.request.translate
200 _ = self.request.translate
162 reason = None
201 reason = None
@@ -676,6 +715,7 b' class BaseReferencesView(RepoAppView):'
676 {
715 {
677 "name": _render("name", ref_name, files_url, closed),
716 "name": _render("name", ref_name, files_url, closed),
678 "name_raw": ref_name,
717 "name_raw": ref_name,
718 "closed": closed,
679 "date": _render("date", commit.date),
719 "date": _render("date", commit.date),
680 "date_raw": datetime_to_time(commit.date),
720 "date_raw": datetime_to_time(commit.date),
681 "author": _render("author", commit.author),
721 "author": _render("author", commit.author),
@@ -446,8 +446,8 b' class TestAdminRepos(object):'
446 csrf_token=csrf_token))
446 csrf_token=csrf_token))
447
447
448 response.mustcontain(
448 response.mustcontain(
449 u"You do not have the permission to store repositories in "
449 "You do not have the permission to store repositories in "
450 u"the root location.")
450 "the root location.")
451
451
452 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
452 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
453 def test_create_repo_when_filesystem_op_fails(
453 def test_create_repo_when_filesystem_op_fails(
@@ -485,7 +485,7 b' class TestAdminSystemInfo(object):'
485 update_data = {
485 update_data = {
486 'versions': [
486 'versions': [
487 {
487 {
488 'version': '100.3.1415926535',
488 'version': '100.0.0',
489 'general': 'The latest version we are ever going to ship'
489 'general': 'The latest version we are ever going to ship'
490 },
490 },
491 {
491 {
@@ -502,15 +502,15 b' class TestAdminSystemInfo(object):'
502 update_data = {
502 update_data = {
503 'versions': [
503 'versions': [
504 {
504 {
505 'version': '0.0.0',
505 'version': '4.0.0',
506 'general': 'The first version we ever shipped'
506 'general': 'The first version we ever shipped'
507 }
507 }
508 ]
508 ]
509 }
509 }
510 text = f"Your current version, {rhodecode.__version__}, is up-to-date as it is equal to or newer than the latest available version, 4.0.0."
510 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
511 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
511 response = self.app.get(route_path('admin_settings_system_update'))
512 response = self.app.get(route_path('admin_settings_system_update'))
512 response.mustcontain(
513 response.mustcontain(text)
513 'This instance is already running the <b>latest</b> stable version')
514
514
515 def test_system_update_bad_response(self, autologin_user):
515 def test_system_update_bad_response(self, autologin_user):
516 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
516 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
@@ -28,7 +28,7 b' from pyramid.renderers import render'
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
31 from rhodecode.apps.ssh_support.events import SshKeyFileChangeEvent
32 from rhodecode import events
32 from rhodecode import events
33
33
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
@@ -32,13 +32,13 b' from pyramid.response import Response'
32
32
33 from rhodecode.apps._base import BaseAppView
33 from rhodecode.apps._base import BaseAppView
34 from rhodecode.apps._base.navigation import navigation_list
34 from rhodecode.apps._base.navigation import navigation_list
35 from rhodecode.apps.svn_support.config_keys import generate_config
35 from rhodecode.apps.svn_support import config_keys
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 from rhodecode.lib.celerylib import tasks, run_task
39 from rhodecode.lib.celerylib import tasks, run_task
40 from rhodecode.lib.str_utils import safe_str
40 from rhodecode.lib.str_utils import safe_str
41 from rhodecode.lib.utils import repo2db_mapper
41 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path
42 from rhodecode.lib.utils2 import str2bool, AttributeDict
42 from rhodecode.lib.utils2 import str2bool, AttributeDict
43 from rhodecode.lib.index import searcher_from_config
43 from rhodecode.lib.index import searcher_from_config
44
44
@@ -113,10 +113,8 b' class AdminSettingsView(BaseAppView):'
113 model = VcsSettingsModel()
113 model = VcsSettingsModel()
114 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
114 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
115 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
115 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
116
116 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
117 settings = self.request.registry.settings
117 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
118 c.svn_proxy_generate_config = settings[generate_config]
119
120 defaults = self._form_defaults()
118 defaults = self._form_defaults()
121
119
122 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
120 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
@@ -143,9 +141,8 b' class AdminSettingsView(BaseAppView):'
143 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
141 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
144 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
142 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
145
143
146 settings = self.request.registry.settings
144 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
147 c.svn_proxy_generate_config = settings[generate_config]
145 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
148
149 application_form = ApplicationUiSettingsForm(self.request.translate)()
146 application_form = ApplicationUiSettingsForm(self.request.translate)()
150
147
151 try:
148 try:
@@ -167,9 +164,6 b' class AdminSettingsView(BaseAppView):'
167 return Response(html)
164 return Response(html)
168
165
169 try:
166 try:
170 if c.visual.allow_repo_location_change:
171 model.update_global_path_setting(form_result['paths_root_path'])
172
173 model.update_global_ssl_setting(form_result['web_push_ssl'])
167 model.update_global_ssl_setting(form_result['web_push_ssl'])
174 model.update_global_hook_settings(form_result)
168 model.update_global_hook_settings(form_result)
175
169
@@ -217,7 +211,7 b' class AdminSettingsView(BaseAppView):'
217 def settings_mapping(self):
211 def settings_mapping(self):
218 c = self.load_default_context()
212 c = self.load_default_context()
219 c.active = 'mapping'
213 c.active = 'mapping'
220 c.storage_path = VcsSettingsModel().get_repos_location()
214 c.storage_path = get_rhodecode_repo_store_path()
221 data = render('rhodecode:templates/admin/settings/settings.mako',
215 data = render('rhodecode:templates/admin/settings/settings.mako',
222 self._get_template_context(c), self.request)
216 self._get_template_context(c), self.request)
223 html = formencode.htmlfill.render(
217 html = formencode.htmlfill.render(
@@ -165,15 +165,20 b' class AdminSystemInfoSettingsView(BaseAp'
165 (_('Storage location'), val('storage')['path'], state('storage')),
165 (_('Storage location'), val('storage')['path'], state('storage')),
166 (_('Storage info'), val('storage')['text'], state('storage')),
166 (_('Storage info'), val('storage')['text'], state('storage')),
167 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
167 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
168 ('', '', ''), # spacer
168
169
169 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
170 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
170 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
171 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
172 ('', '', ''), # spacer
171
173
174 (_('Archive cache storage type'), val('storage_archive')['type'], state('storage_archive')),
172 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
175 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
173 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
176 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
177 ('', '', ''), # spacer
174
178
175 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
179 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
176 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
180 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
181 ('', '', ''), # spacer
177
182
178 (_('Search info'), val('search')['text'], state('search')),
183 (_('Search info'), val('search')['text'], state('search')),
179 (_('Search location'), val('search')['location'], state('search')),
184 (_('Search location'), val('search')['location'], state('search')),
@@ -209,7 +214,8 b' class AdminSystemInfoSettingsView(BaseAp'
209 update_url = UpdateModel().get_update_url()
214 update_url = UpdateModel().get_update_url()
210
215
211 def _err(s):
216 def _err(s):
212 return '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
217 return f'<div style="color:#ff8888; padding:4px 0px">{s}</div>'
218
213 try:
219 try:
214 data = UpdateModel().get_update_data(update_url)
220 data = UpdateModel().get_update_data(update_url)
215 except urllib.error.URLError as e:
221 except urllib.error.URLError as e:
@@ -225,12 +231,12 b' class AdminSystemInfoSettingsView(BaseAp'
225
231
226 c.update_url = update_url
232 c.update_url = update_url
227 c.latest_data = latest
233 c.latest_data = latest
228 c.latest_ver = latest['version']
234 c.latest_ver = (latest['version'] or '').strip()
229 c.cur_ver = rhodecode.__version__
235 c.cur_ver = self.request.GET.get('ver') or rhodecode.__version__
230 c.should_upgrade = False
236 c.should_upgrade = False
231
237
232 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
238 is_outdated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
233 if is_oudated:
239 if is_outdated:
234 c.should_upgrade = True
240 c.should_upgrade = True
235 c.important_notices = latest['general']
241 c.important_notices = latest['general']
236 UpdateModel().store_version(latest['version'])
242 UpdateModel().store_version(latest['version'])
@@ -27,7 +27,7 b' from pyramid.response import Response'
27
27
28 from rhodecode import events
28 from rhodecode import events
29 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
29 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
30 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
30 from rhodecode.apps.ssh_support.events import SshKeyFileChangeEvent
31 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
31 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
32 from rhodecode.authentication.plugins import auth_rhodecode
32 from rhodecode.authentication.plugins import auth_rhodecode
33 from rhodecode.events import trigger
33 from rhodecode.events import trigger
@@ -214,7 +214,7 b' class AdminUsersView(BaseAppView, DataGr'
214 html = formencode.htmlfill.render(
214 html = formencode.htmlfill.render(
215 data,
215 data,
216 defaults=errors.value,
216 defaults=errors.value,
217 errors=errors.unpack_errors() or {},
217 errors=errors.error_dict or {},
218 prefix_error=False,
218 prefix_error=False,
219 encoding="UTF-8",
219 encoding="UTF-8",
220 force_defaults=False
220 force_defaults=False
@@ -75,3 +75,27 b' def includeme(config):'
75 LoginView,
75 LoginView,
76 attr='password_reset_confirmation',
76 attr='password_reset_confirmation',
77 route_name='reset_password_confirmation', request_method='GET')
77 route_name='reset_password_confirmation', request_method='GET')
78
79 config.add_route(
80 name='setup_2fa',
81 pattern=ADMIN_PREFIX + '/setup_2fa')
82 config.add_view(
83 LoginView,
84 attr='setup_2fa',
85 route_name='setup_2fa', request_method=['GET', 'POST'],
86 renderer='rhodecode:templates/configure_2fa.mako')
87
88 config.add_route(
89 name='check_2fa',
90 pattern=ADMIN_PREFIX + '/check_2fa')
91 config.add_view(
92 LoginView,
93 attr='verify_2fa',
94 route_name='check_2fa', request_method='GET',
95 renderer='rhodecode:templates/verify_2fa.mako')
96 config.add_view(
97 LoginView,
98 attr='verify_2fa',
99 route_name='check_2fa', request_method='POST',
100 renderer='rhodecode:templates/verify_2fa.mako')
101
@@ -80,6 +80,18 b' class TestLoginController(object):'
80 assert username == 'test_regular'
80 assert username == 'test_regular'
81 response.mustcontain('logout')
81 response.mustcontain('logout')
82
82
83 def test_login_with_primary_email(self):
84 user_email = 'test_regular@mail.com'
85 response = self.app.post(route_path('login'),
86 {'username': user_email,
87 'password': 'test12'}, status=302)
88 response = response.follow()
89 session = response.get_session_from_response()
90 user = session['rhodecode_user']
91 assert user['username'] == user_email.split('@')[0]
92 assert user['is_authenticated']
93 response.mustcontain('logout')
94
83 def test_login_regular_forbidden_when_super_admin_restriction(self):
95 def test_login_regular_forbidden_when_super_admin_restriction(self):
84 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
96 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
85 with fixture.auth_restriction(self.app._pyramid_registry,
97 with fixture.auth_restriction(self.app._pyramid_registry,
@@ -254,7 +266,7 b' class TestLoginController(object):'
254 )
266 )
255
267
256 assertr = response.assert_response()
268 assertr = response.assert_response()
257 msg = u'This e-mail address is already taken'
269 msg = 'This e-mail address is already taken'
258 assertr.element_contains('#email+.error-message', msg)
270 assertr.element_contains('#email+.error-message', msg)
259
271
260 def test_register_err_same_email_case_sensitive(self):
272 def test_register_err_same_email_case_sensitive(self):
@@ -270,7 +282,7 b' class TestLoginController(object):'
270 }
282 }
271 )
283 )
272 assertr = response.assert_response()
284 assertr = response.assert_response()
273 msg = u'This e-mail address is already taken'
285 msg = 'This e-mail address is already taken'
274 assertr.element_contains('#email+.error-message', msg)
286 assertr.element_contains('#email+.error-message', msg)
275
287
276 def test_register_err_wrong_data(self):
288 def test_register_err_wrong_data(self):
@@ -423,7 +435,7 b' class TestLoginController(object):'
423 'If such email exists, a password reset link was sent to it.')
435 'If such email exists, a password reset link was sent to it.')
424
436
425 # BAD KEY
437 # BAD KEY
426 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
438 confirm_url = route_path('reset_password_confirmation', params={'key': 'badkey'})
427 response = self.app.get(confirm_url, status=302)
439 response = self.app.get(confirm_url, status=302)
428 assert response.location.endswith(route_path('reset_password'))
440 assert response.location.endswith(route_path('reset_password'))
429 assert_session_flash(response, 'Given reset token is invalid')
441 assert_session_flash(response, 'Given reset token is invalid')
@@ -17,6 +17,9 b''
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import time
19 import time
20 import json
21 import pyotp
22 import qrcode
20 import collections
23 import collections
21 import datetime
24 import datetime
22 import formencode
25 import formencode
@@ -24,10 +27,14 b' import formencode.htmlfill'
24 import logging
27 import logging
25 import urllib.parse
28 import urllib.parse
26 import requests
29 import requests
30 from io import BytesIO
31 from base64 import b64encode
27
32
33 from pyramid.renderers import render
34 from pyramid.response import Response
28 from pyramid.httpexceptions import HTTPFound
35 from pyramid.httpexceptions import HTTPFound
29
36
30
37 import rhodecode
31 from rhodecode.apps._base import BaseAppView
38 from rhodecode.apps._base import BaseAppView
32 from rhodecode.authentication.base import authenticate, HTTP_TYPE
39 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 from rhodecode.authentication.plugins import auth_rhodecode
40 from rhodecode.authentication.plugins import auth_rhodecode
@@ -35,12 +42,12 b' from rhodecode.events import UserRegiste'
35 from rhodecode.lib import helpers as h
42 from rhodecode.lib import helpers as h
36 from rhodecode.lib import audit_logger
43 from rhodecode.lib import audit_logger
37 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
45 AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous)
39 from rhodecode.lib.base import get_ip_addr
46 from rhodecode.lib.base import get_ip_addr
40 from rhodecode.lib.exceptions import UserCreationError
47 from rhodecode.lib.exceptions import UserCreationError
41 from rhodecode.lib.utils2 import safe_str
48 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.model.db import User, UserApiKeys
49 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
50 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
44 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
45 from rhodecode.model.auth_token import AuthTokenModel
52 from rhodecode.model.auth_token import AuthTokenModel
46 from rhodecode.model.settings import SettingsModel
53 from rhodecode.model.settings import SettingsModel
@@ -54,8 +61,8 b' CaptchaData = collections.namedtuple('
54 'CaptchaData', 'active, private_key, public_key')
61 'CaptchaData', 'active, private_key, public_key')
55
62
56
63
57 def store_user_in_session(session, username, remember=False):
64 def store_user_in_session(session, user_identifier, remember=False):
58 user = User.get_by_username(username, case_insensitive=True)
65 user = User.get_by_username_or_primary_email(user_identifier)
59 auth_user = AuthUser(user.user_id)
66 auth_user = AuthUser(user.user_id)
60 auth_user.set_authenticated()
67 auth_user.set_authenticated()
61 cs = auth_user.get_cookie_store()
68 cs = auth_user.get_cookie_store()
@@ -74,7 +81,7 b' def store_user_in_session(session, usern'
74 safe_cs = cs.copy()
81 safe_cs = cs.copy()
75 safe_cs['password'] = '****'
82 safe_cs['password'] = '****'
76 log.info('user %s is now authenticated and stored in '
83 log.info('user %s is now authenticated and stored in '
77 'session, session attrs %s', username, safe_cs)
84 'session, session attrs %s', user_identifier, safe_cs)
78
85
79 # dumps session attrs back to cookie
86 # dumps session attrs back to cookie
80 session._update_cookie_out()
87 session._update_cookie_out()
@@ -179,9 +186,13 b' class LoginView(BaseAppView):'
179 self.session.invalidate()
186 self.session.invalidate()
180 form_result = login_form.to_python(self.request.POST)
187 form_result = login_form.to_python(self.request.POST)
181 # form checks for username/password, now we're authenticated
188 # form checks for username/password, now we're authenticated
189 username = form_result['username']
190 if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa:
191 user.check_2fa_required = True
192
182 headers = store_user_in_session(
193 headers = store_user_in_session(
183 self.session,
194 self.session,
184 username=form_result['username'],
195 user_identifier=username,
185 remember=form_result['remember'])
196 remember=form_result['remember'])
186 log.debug('Redirecting to "%s" after login.', c.came_from)
197 log.debug('Redirecting to "%s" after login.', c.came_from)
187
198
@@ -438,12 +449,12 b' class LoginView(BaseAppView):'
438
449
439 def password_reset_confirmation(self):
450 def password_reset_confirmation(self):
440 self.load_default_context()
451 self.load_default_context()
441 if self.request.GET and self.request.GET.get('key'):
452
453 if key := self.request.GET.get('key'):
442 # make this take 2s, to prevent brute forcing.
454 # make this take 2s, to prevent brute forcing.
443 time.sleep(2)
455 time.sleep(2)
444
456
445 token = AuthTokenModel().get_auth_token(
457 token = AuthTokenModel().get_auth_token(key)
446 self.request.GET.get('key'))
447
458
448 # verify token is the correct role
459 # verify token is the correct role
449 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
460 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
@@ -467,3 +478,76 b' class LoginView(BaseAppView):'
467 return HTTPFound(self.request.route_path('reset_password'))
478 return HTTPFound(self.request.route_path('reset_password'))
468
479
469 return HTTPFound(self.request.route_path('login'))
480 return HTTPFound(self.request.route_path('login'))
481
482 @LoginRequired()
483 @NotAnonymous()
484 def setup_2fa(self):
485 _ = self.request.translate
486 c = self.load_default_context()
487 user_instance = self._rhodecode_db_user
488 form = TOTPForm(_, user_instance)()
489 render_ctx = {}
490 if self.request.method == 'POST':
491 post_items = dict(self.request.POST)
492
493 try:
494 form_details = form.to_python(post_items)
495 secret = form_details['secret_totp']
496
497 user_instance.init_2fa_recovery_codes(persist=True, force=True)
498 user_instance.secret_2fa = secret
499
500 Session().commit()
501 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
502 except formencode.Invalid as errors:
503 defaults = errors.value
504 render_ctx = {
505 'errors': errors.error_dict,
506 'defaults': defaults,
507 }
508
509 # NOTE: here we DO NOT persist the secret 2FA, since this is only for setup, once a setup is completed
510 # only then we should persist it
511 secret = user_instance.init_secret_2fa(persist=False)
512
513 instance_name = rhodecode.ConfigGet().get_str('app.base_url', 'rhodecode')
514 totp_name = f'{instance_name}:{self.request.user.username}'
515
516 qr = qrcode.QRCode(version=1, box_size=5, border=4)
517 qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name))
518 qr.make(fit=True)
519 img = qr.make_image(fill_color='black', back_color='white')
520 buffered = BytesIO()
521 img.save(buffered)
522 return self._get_template_context(
523 c,
524 qr=b64encode(buffered.getvalue()).decode("utf-8"),
525 key=secret,
526 totp_name=totp_name,
527 ** render_ctx
528 )
529
530 @LoginRequired()
531 @NotAnonymous()
532 def verify_2fa(self):
533 _ = self.request.translate
534 c = self.load_default_context()
535 render_ctx = {}
536 user_instance = self._rhodecode_db_user
537 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
538 if self.request.method == 'POST':
539 post_items = dict(self.request.POST)
540 # NOTE: inject secret, as it's a post configured saved item.
541 post_items['secret_totp'] = user_instance.secret_2fa
542 try:
543 totp_form.to_python(post_items)
544 user_instance.check_2fa_required = False
545 Session().commit()
546 raise HTTPFound(c.came_from)
547 except formencode.Invalid as errors:
548 defaults = errors.value
549 render_ctx = {
550 'errors': errors.error_dict,
551 'defaults': defaults,
552 }
553 return self._get_template_context(c, **render_ctx)
@@ -74,6 +74,45 b' def includeme(config):'
74 route_name='my_account_password_update', request_method='POST',
74 route_name='my_account_password_update', request_method='POST',
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76
76
77 # my account 2fa
78 config.add_route(
79 name='my_account_configure_2fa',
80 pattern=ADMIN_PREFIX + '/my_account/configure_2fa')
81 config.add_view(
82 MyAccountView,
83 attr='my_account_2fa',
84 route_name='my_account_configure_2fa', request_method='GET',
85 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86 # my account 2fa save
87 config.add_route(
88 name='my_account_configure_2fa_update',
89 pattern=ADMIN_PREFIX + '/my_account/configure_2fa_update')
90 config.add_view(
91 MyAccountView,
92 attr='my_account_2fa_update',
93 route_name='my_account_configure_2fa_update', request_method='POST',
94 renderer='rhodecode:templates/admin/my_account/my_account.mako')
95
96 # my account 2fa recovery code-reset
97 config.add_route(
98 name='my_account_show_2fa_recovery_codes',
99 pattern=ADMIN_PREFIX + '/my_account/recovery_codes')
100 config.add_view(
101 MyAccountView,
102 attr='my_account_2fa_show_recovery_codes',
103 route_name='my_account_show_2fa_recovery_codes', request_method='POST', xhr=True,
104 renderer='json_ext')
105
106 # my account 2fa recovery code-reset
107 config.add_route(
108 name='my_account_regenerate_2fa_recovery_codes',
109 pattern=ADMIN_PREFIX + '/my_account/regenerate_recovery_codes')
110 config.add_view(
111 MyAccountView,
112 attr='my_account_2fa_regenerate_recovery_codes',
113 route_name='my_account_regenerate_2fa_recovery_codes', request_method='POST',
114 renderer='rhodecode:templates/admin/my_account/my_account.mako')
115
77 # my account tokens
116 # my account tokens
78 config.add_route(
117 config.add_route(
79 name='my_account_auth_tokens',
118 name='my_account_auth_tokens',
@@ -16,6 +16,7 b''
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import time
19 import logging
20 import logging
20 import datetime
21 import datetime
21 import string
22 import string
@@ -40,9 +41,10 b' from rhodecode.lib.utils2 import safe_in'
40 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.db import (
43 from rhodecode.model.db import (
43 IntegrityError, or_, in_filter_generator,
44 IntegrityError, or_, in_filter_generator, select,
44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 from rhodecode.model.forms import TOTPForm
46 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
47 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
@@ -136,6 +138,7 b' class MyAccountView(BaseAppView, DataGri'
136 except forms.ValidationFailure as e:
138 except forms.ValidationFailure as e:
137 c.form = e
139 c.form = e
138 return self._get_template_context(c)
140 return self._get_template_context(c)
141
139 except Exception:
142 except Exception:
140 log.exception("Exception updating user")
143 log.exception("Exception updating user")
141 h.flash(_('Error occurred during update of user'),
144 h.flash(_('Error occurred during update of user'),
@@ -203,6 +206,74 b' class MyAccountView(BaseAppView, DataGri'
203
206
204 @LoginRequired()
207 @LoginRequired()
205 @NotAnonymous()
208 @NotAnonymous()
209 def my_account_2fa(self):
210 _ = self.request.translate
211 c = self.load_default_context()
212 c.active = '2fa'
213 user_instance = c.auth_user.get_instance()
214 locked_by_admin = user_instance.has_forced_2fa
215 c.state_of_2fa = user_instance.has_enabled_2fa
216 c.user_seen_2fa_recovery_codes = user_instance.has_seen_2fa_codes
217 c.locked_2fa = str2bool(locked_by_admin)
218 return self._get_template_context(c)
219
220 @LoginRequired()
221 @NotAnonymous()
222 @CSRFRequired()
223 def my_account_2fa_update(self):
224 _ = self.request.translate
225 c = self.load_default_context()
226 c.active = '2fa'
227 user_instance = c.auth_user.get_instance()
228
229 state = str2bool(self.request.POST.get('2fa_status'))
230 user_instance.has_enabled_2fa = state
231 user_instance.update_userdata(update_2fa=time.time())
232 Session().commit()
233 if state:
234 h.flash(_("2FA has been successfully enabled"), category='success')
235 else:
236 h.flash(_("2FA has been successfully disabled"), category='success')
237 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
238
239 @LoginRequired()
240 @NotAnonymous()
241 @CSRFRequired()
242 def my_account_2fa_show_recovery_codes(self):
243 c = self.load_default_context()
244 user_instance = c.auth_user.get_instance()
245 user_instance.has_seen_2fa_codes = True
246 Session().commit()
247 return {'recovery_codes': user_instance.get_2fa_recovery_codes()}
248
249 @LoginRequired()
250 @NotAnonymous()
251 @CSRFRequired()
252 def my_account_2fa_regenerate_recovery_codes(self):
253 _ = self.request.translate
254 c = self.load_default_context()
255 user_instance = c.auth_user.get_instance()
256
257 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
258
259 post_items = dict(self.request.POST)
260 # NOTE: inject secret, as it's a post configured saved item.
261 post_items['secret_totp'] = user_instance.secret_2fa
262 try:
263 totp_form.to_python(post_items)
264 user_instance.regenerate_2fa_recovery_codes()
265 Session().commit()
266 except formencode.Invalid as errors:
267 h.flash(_("Failed to generate new recovery codes: {}").format(errors), category='error')
268 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
269 except Exception as e:
270 h.flash(_("Failed to generate new recovery codes: {}").format(e), category='error')
271 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
272
273 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
274
275 @LoginRequired()
276 @NotAnonymous()
206 def my_account_auth_tokens(self):
277 def my_account_auth_tokens(self):
207 _ = self.request.translate
278 _ = self.request.translate
208
279
@@ -483,8 +554,15 b' class MyAccountView(BaseAppView, DataGri'
483 def my_account_bookmarks(self):
554 def my_account_bookmarks(self):
484 c = self.load_default_context()
555 c = self.load_default_context()
485 c.active = 'bookmarks'
556 c.active = 'bookmarks'
486 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
557
487 self._rhodecode_db_user.user_id, cache=False)
558 user_bookmarks = \
559 select(UserBookmark, Repository, RepoGroup) \
560 .where(UserBookmark.user_id == self._rhodecode_user.user_id) \
561 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
562 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
563 .order_by(UserBookmark.position.asc())
564
565 c.user_bookmark_items = Session().execute(user_bookmarks).all()
488 return self._get_template_context(c)
566 return self._get_template_context(c)
489
567
490 def _process_bookmark_entry(self, entry, user_id):
568 def _process_bookmark_entry(self, entry, user_id):
@@ -21,7 +21,7 b' import logging'
21 from pyramid.httpexceptions import HTTPFound
21 from pyramid.httpexceptions import HTTPFound
22
22
23 from rhodecode.apps._base import BaseAppView, DataGridAppView
23 from rhodecode.apps._base import BaseAppView, DataGridAppView
24 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
24 from rhodecode.apps.ssh_support.events import SshKeyFileChangeEvent
25 from rhodecode.events import trigger
25 from rhodecode.events import trigger
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib import audit_logger
27 from rhodecode.lib import audit_logger
@@ -41,6 +41,15 b' def admin_routes(config):'
41 renderer='json_ext')
41 renderer='json_ext')
42
42
43 config.add_route(
43 config.add_route(
44 name='ops_celery_error_test',
45 pattern='/error-celery')
46 config.add_view(
47 OpsView,
48 attr='ops_celery_error_test',
49 route_name='ops_celery_error_test', request_method='GET',
50 renderer='json_ext')
51
52 config.add_route(
44 name='ops_redirect_test',
53 name='ops_redirect_test',
45 pattern='/redirect')
54 pattern='/redirect')
46 config.add_view(
55 config.add_view(
@@ -66,6 +66,20 b' class OpsView(BaseAppView):'
66 'Client:{}. Generation time: {}.'.format(self.request.user, time.time()))
66 'Client:{}. Generation time: {}.'.format(self.request.user, time.time()))
67 raise TestException(msg)
67 raise TestException(msg)
68
68
69 def ops_celery_error_test(self):
70 """
71 Test exception handling and emails on errors
72 """
73 from rhodecode.lib.celerylib import tasks, run_task
74
75 # add timeout so we add some sort of rate limiter
76 time.sleep(2)
77
78 msg = ('RhodeCode Enterprise test exception. '
79 'Client:{}. Generation time: {}.'.format(self.request.user, time.time()))
80 celery_task = run_task(tasks.test_celery_exception, msg)
81 return {'task': str(celery_task)}
82
69 def ops_redirect_test(self):
83 def ops_redirect_test(self):
70 """
84 """
71 Test redirect handling
85 Test redirect handling
@@ -591,6 +591,15 b' def includeme(config):'
591 route_name='branches_home', request_method='GET',
591 route_name='branches_home', request_method='GET',
592 renderer='rhodecode:templates/branches/branches.mako')
592 renderer='rhodecode:templates/branches/branches.mako')
593
593
594 config.add_route(
595 name='branch_remove',
596 pattern='/{repo_name:.*?[^/]}/branches/{branch_name:.*?[^/]}/remove', repo_route=True, repo_accepted_types=['hg', 'git'])
597 config.add_view(
598 RepoBranchesView,
599 attr='remove_branch',
600 route_name='branch_remove', request_method='POST'
601 )
602
594 # Bookmarks
603 # Bookmarks
595 config.add_route(
604 config.add_route(
596 name='bookmarks_home',
605 name='bookmarks_home',
@@ -19,6 +19,7 b''
19 import pytest
19 import pytest
20 from rhodecode.model.db import Repository
20 from rhodecode.model.db import Repository
21 from rhodecode.tests.routes import route_path
21 from rhodecode.tests.routes import route_path
22 from rhodecode.tests import assert_session_flash
22
23
23
24
24 @pytest.mark.usefixtures('autologin_user', 'app')
25 @pytest.mark.usefixtures('autologin_user', 'app')
@@ -33,3 +34,50 b' class TestBranchesController(object):'
33 for commit_id, obj_name in repo.scm_instance().branches.items():
34 for commit_id, obj_name in repo.scm_instance().branches.items():
34 assert commit_id in response
35 assert commit_id in response
35 assert obj_name in response
36 assert obj_name in response
37
38 def test_landing_branch_delete(self, backend, csrf_token):
39 if backend.alias == 'svn':
40 pytest.skip("Not supported yet")
41 branch_related_data_per_backend = {
42 'git': {'name': 'master'},
43 'hg': {'name': 'default'},
44 }
45 response = self.app.post(
46 route_path('branch_remove', repo_name=backend.repo_name,
47 branch_name=branch_related_data_per_backend[backend.alias]['name']),
48 params={'csrf_token': csrf_token}, status=302)
49 assert_session_flash(
50 response,
51 f"This branch {branch_related_data_per_backend[backend.alias]['name']} cannot be removed as it's currently set as landing branch"
52 )
53
54 def test_delete_branch_by_repo_owner(self, backend, csrf_token):
55 if backend.alias in ('svn', 'hg'):
56 pytest.skip("Skipping for hg and svn")
57 branch_to_be_removed = 'remove_me'
58 repo = Repository.get_by_repo_name(backend.repo_name)
59 repo.scm_instance()._create_branch(branch_to_be_removed, repo.scm_instance().commit_ids[1])
60 response = self.app.post(
61 route_path('branch_remove', repo_name=backend.repo_name,
62 branch_name=branch_to_be_removed),
63 params={'csrf_token': csrf_token}, status=302)
64 assert_session_flash(response, f"Branch {branch_to_be_removed} has been successfully deleted")
65
66 def test_delete_branch_by_not_repo_owner(self, backend, csrf_token):
67 username = 'test_regular'
68 pwd = 'test12'
69 branch_related_data_per_backend = {
70 'git': {'name': 'master', 'action': 'deleted'},
71 'hg': {'name': 'stable', 'action': 'closed'},
72 }
73 if backend.alias == 'svn':
74 pytest.skip("Not supported yet")
75 self.app.post(route_path('login'),
76 {'username': username,
77 'password': pwd})
78 selected_branch = branch_related_data_per_backend[backend.alias]['name']
79 response = self.app.post(
80 route_path('branch_remove', repo_name=backend.repo_name,
81 branch_name=selected_branch),
82 params={'csrf_token': csrf_token, 'username': username, 'password': pwd}, status=404)
83 assert response.status_code == 404
@@ -1,4 +1,3 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
@@ -52,11 +51,11 b' def assert_clone_url(response, server, r'
52
51
53 @pytest.mark.usefixtures('app')
52 @pytest.mark.usefixtures('app')
54 class TestSummaryView(object):
53 class TestSummaryView(object):
54
55 def test_index(self, autologin_user, backend, http_host_only_stub):
55 def test_index(self, autologin_user, backend, http_host_only_stub):
56 repo_id = backend.repo.repo_id
56 repo_id = backend.repo.repo_id
57 repo_name = backend.repo_name
57 repo_name = backend.repo_name
58 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
58
59 return_value=False):
60 response = self.app.get(
59 response = self.app.get(
61 route_path('repo_summary', repo_name=repo_name))
60 route_path('repo_summary', repo_name=repo_name))
62
61
@@ -71,37 +70,43 b' class TestSummaryView(object):'
71
70
72 # clone url...
71 # clone url...
73 assert_clone_url(response, http_host_only_stub, repo_name)
72 assert_clone_url(response, http_host_only_stub, repo_name)
74 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id))
73 assert_clone_url(response, http_host_only_stub, f'_{repo_id}')
75
74
76 def test_index_svn_without_proxy(
75 def test_index_svn_without_proxy(
77 self, autologin_user, backend_svn, http_host_only_stub):
76 self, autologin_user, backend_svn, http_host_only_stub):
77
78 repo_id = backend_svn.repo.repo_id
78 repo_id = backend_svn.repo.repo_id
79 repo_name = backend_svn.repo_name
79 repo_name = backend_svn.repo_name
80 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
80
81 # clone url...
81 # by default the SVN is enabled now, this is how inputs look when it's disabled
82 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', return_value=True):
82
83
84 response = self.app.get(
85 route_path('repo_summary', repo_name=repo_name),
86 status=200)
87
88 # clone url test...
83 assert_clone_url(response, http_host_only_stub, repo_name, disabled=True)
89 assert_clone_url(response, http_host_only_stub, repo_name, disabled=True)
84 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id), disabled=True)
90 assert_clone_url(response, http_host_only_stub, f'_{repo_id}', disabled=True)
85
91
86 def test_index_with_trailing_slash(
92 def test_index_with_trailing_slash(
87 self, autologin_user, backend, http_host_only_stub):
93 self, autologin_user, backend, http_host_only_stub):
88
94
89 repo_id = backend.repo.repo_id
95 repo_id = backend.repo.repo_id
90 repo_name = backend.repo_name
96 repo_name = backend.repo_name
91 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
97 trailing_slash = '/'
92 return_value=False):
93 response = self.app.get(
98 response = self.app.get(
94 route_path('repo_summary', repo_name=repo_name) + '/',
99 route_path('repo_summary', repo_name=repo_name) + trailing_slash,
95 status=200)
100 status=200)
96
101
97 # clone url...
102 # clone url...
98 assert_clone_url(response, http_host_only_stub, repo_name)
103 assert_clone_url(response, http_host_only_stub, repo_name)
99 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id))
104 assert_clone_url(response, http_host_only_stub, f'_{repo_id}')
100
105
101 def test_index_by_id(self, autologin_user, backend):
106 def test_index_by_id(self, autologin_user, backend):
102 repo_id = backend.repo.repo_id
107 repo_id = backend.repo.repo_id
103 response = self.app.get(
108 response = self.app.get(
104 route_path('repo_summary', repo_name='_%s' % (repo_id,)))
109 route_path('repo_summary', repo_name=f'_{repo_id}'))
105
110
106 # repo type
111 # repo type
107 response.mustcontain(
112 response.mustcontain(
@@ -18,11 +18,15 b''
18
18
19 import logging
19 import logging
20
20
21 from pyramid.httpexceptions import HTTPFound
21
22
22 from rhodecode.apps._base import BaseReferencesView
23 from rhodecode.apps._base import BaseReferencesView
23 from rhodecode.lib import ext_json
24 from rhodecode.lib import ext_json
24 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
25 from rhodecode.lib import helpers as h
26 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
25 from rhodecode.model.scm import ScmModel
27 from rhodecode.model.scm import ScmModel
28 from rhodecode.model.meta import Session
29 from rhodecode.model.db import PullRequest
26
30
27 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
28
32
@@ -33,15 +37,71 b' class RepoBranchesView(BaseReferencesVie'
33 @HasRepoPermissionAnyDecorator(
37 @HasRepoPermissionAnyDecorator(
34 'repository.read', 'repository.write', 'repository.admin')
38 'repository.read', 'repository.write', 'repository.admin')
35 def branches(self):
39 def branches(self):
40 partial_render = self.request.get_partial_renderer(
41 'rhodecode:templates/data_table/_dt_elements.mako')
42 repo_name = self.db_repo_name
36 c = self.load_default_context()
43 c = self.load_default_context()
37 self._prepare_and_set_clone_url(c)
44 self._prepare_and_set_clone_url(c)
38 c.rhodecode_repo = self.rhodecode_vcs_repo
45 c.rhodecode_repo = self.rhodecode_vcs_repo
39 c.repository_forks = ScmModel().get_forks(self.db_repo)
46 c.repository_forks = ScmModel().get_forks(self.db_repo)
40
41 ref_items = self.rhodecode_vcs_repo.branches_all.items()
47 ref_items = self.rhodecode_vcs_repo.branches_all.items()
42 data = self.load_refs_context(
48 data = self.load_refs_context(
43 ref_items=ref_items, partials_template='branches/branches_data.mako')
49 ref_items=ref_items, partials_template='branches/branches_data.mako')
44
50 data_with_actions = []
51 if self.db_repo.repo_type != 'svn':
52 for branch in data:
53 branch['action'] = partial_render(
54 f"branch_actions_{self.db_repo.repo_type}", branch['name_raw'], repo_name, closed=branch['closed']
55 )
56 data_with_actions.append(branch)
57 data = data_with_actions
45 c.has_references = bool(data)
58 c.has_references = bool(data)
46 c.data = ext_json.str_json(data)
59 c.data = ext_json.str_json(data)
47 return self._get_template_context(c)
60 return self._get_template_context(c)
61
62 @LoginRequired()
63 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
64 @CSRFRequired()
65 def remove_branch(self):
66 _ = self.request.translate
67 self.load_default_context()
68 repo = self.db_repo
69 repo_name = self.db_repo_name
70 repo_type = repo.repo_type
71 action = _('deleted') if repo_type == 'git' else _('closed')
72 redirect = HTTPFound(location=self.request.route_path('branches_home', repo_name=repo_name))
73 branch_name = self.request.matchdict.get('branch_name')
74 if repo.landing_ref_name == branch_name:
75 h.flash(
76 _("This branch {} cannot be removed as it's currently set as landing branch").format(branch_name),
77 category='error'
78 )
79 return redirect
80 if prs_related_to := Session().query(PullRequest).filter(PullRequest.target_repo_id == repo.repo_id,
81 PullRequest.status != PullRequest.STATUS_CLOSED).filter(
82 (PullRequest.source_ref.like(f'branch:{branch_name}:%')) | (
83 PullRequest.target_ref.like(f'branch:{branch_name}:%'))
84 ).all():
85 h.flash(_("Branch cannot be {} - it's used in following open Pull Request ids: {}").format(action, ','.join(
86 map(str, prs_related_to))), category='error')
87 return redirect
88
89 match repo_type:
90 case 'git':
91 self.rhodecode_vcs_repo.delete_branch(branch_name)
92 case 'hg':
93 from rhodecode.lib.vcs.backends.base import Reference
94 self.rhodecode_vcs_repo._local_close(
95 source_ref=Reference(type='branch', name=branch_name,
96 commit_id=self.rhodecode_vcs_repo.branches[branch_name]),
97 target_ref=Reference(type='branch', name='', commit_id=None),
98 user_name=self.request.user.name,
99 user_email=self.request.user.email)
100 case _:
101 raise NotImplementedError('Branch deleting functionality not yet implemented')
102 ScmModel().mark_for_invalidation(repo_name)
103 self.rhodecode_vcs_repo._invalidate_prop_cache('commit_ids')
104 self.rhodecode_vcs_repo._invalidate_prop_cache('_refs')
105 self.rhodecode_vcs_repo._invalidate_prop_cache('branches')
106 h.flash(_("Branch {} has been successfully {}").format(branch_name, action), category='success')
107 return redirect
@@ -24,6 +24,8 b' import urllib.request'
24 import urllib.parse
24 import urllib.parse
25 import urllib.error
25 import urllib.error
26 import pathlib
26 import pathlib
27 import time
28 import random
27
29
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29
31
@@ -37,7 +39,8 b' from rhodecode.apps._base import RepoApp'
37 from rhodecode.lib import diffs, helpers as h, rc_cache
39 from rhodecode.lib import diffs, helpers as h, rc_cache
38 from rhodecode.lib import audit_logger
40 from rhodecode.lib import audit_logger
39 from rhodecode.lib.hash_utils import sha1_safe
41 from rhodecode.lib.hash_utils import sha1_safe
40 from rhodecode.lib.rc_cache.archive_cache import get_archival_cache_store, get_archival_config, ReentrantLock
42 from rhodecode.lib.archive_cache import (
43 get_archival_cache_store, get_archival_config, ArchiveCacheGenerationLock, archive_iterator)
41 from rhodecode.lib.str_utils import safe_bytes, convert_special_chars
44 from rhodecode.lib.str_utils import safe_bytes, convert_special_chars
42 from rhodecode.lib.view_utils import parse_path_ref
45 from rhodecode.lib.view_utils import parse_path_ref
43 from rhodecode.lib.exceptions import NonRelativePathError
46 from rhodecode.lib.exceptions import NonRelativePathError
@@ -417,22 +420,24 b' class RepoFilesView(RepoAppView):'
417 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
420 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
418 d_cache_conf = get_archival_config(config=CONFIG)
421 d_cache_conf = get_archival_config(config=CONFIG)
419
422
423 # This is also a cache key, and lock key
420 reentrant_lock_key = archive_name_key + '.lock'
424 reentrant_lock_key = archive_name_key + '.lock'
421 with ReentrantLock(d_cache, reentrant_lock_key):
425
422 # This is also a cache key
423 use_cached_archive = False
426 use_cached_archive = False
424 if archive_name_key in d_cache and not archive_cache_disable:
427 if not archive_cache_disable and archive_name_key in d_cache:
425 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
428 reader, metadata = d_cache.fetch(archive_name_key)
429
426 use_cached_archive = True
430 use_cached_archive = True
427 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
431 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
428 archive_name_key, tag, reader.name)
432 archive_name_key, metadata, reader.name)
429 else:
433 else:
430 reader = None
434 reader = None
431 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
435 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
432
436
437 if not reader:
433 # generate new archive, as previous was not found in the cache
438 # generate new archive, as previous was not found in the cache
434 if not reader:
439 try:
435
440 with d_cache.get_lock(reentrant_lock_key):
436 try:
441 try:
437 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
442 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
438 kind=fileformat, subrepos=subrepos,
443 kind=fileformat, subrepos=subrepos,
@@ -440,18 +445,21 b' class RepoFilesView(RepoAppView):'
440 except ImproperArchiveTypeError:
445 except ImproperArchiveTypeError:
441 return _('Unknown archive type')
446 return _('Unknown archive type')
442
447
443 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
448 except ArchiveCacheGenerationLock:
444
449 retry_after = round(random.uniform(0.3, 3.0), 1)
445 if not reader:
450 time.sleep(retry_after)
446 raise ValueError('archive cache reader is empty, failed to fetch file from distributed archive cache')
447
451
448 def archive_iterator(_reader, block_size: int = 4096*512):
452 location = self.request.url
449 # 4096 * 64 = 64KB
453 response = Response(
450 while 1:
454 f"archive {archive_name_key} generation in progress, Retry-After={retry_after}, Location={location}"
451 data = _reader.read(block_size)
455 )
452 if not data:
456 response.headers["Retry-After"] = str(retry_after)
453 break
457 response.status_code = 307 # temporary redirect
454 yield data
458
459 response.location = location
460 return response
461
462 reader, metadata = d_cache.fetch(archive_name_key, retry=True, retry_attempts=30)
455
463
456 response = Response(app_iter=archive_iterator(reader))
464 response = Response(app_iter=archive_iterator(reader))
457 response.content_disposition = f'attachment; filename={response_archive_name}'
465 response.content_disposition = f'attachment; filename={response_archive_name}'
@@ -24,7 +24,9 b' from pyramid.httpexceptions import HTTPF'
24 from pyramid.response import Response
24 from pyramid.response import Response
25 from pyramid.renderers import render
25 from pyramid.renderers import render
26
26
27 import rhodecode
27 from rhodecode.apps._base import RepoAppView
28 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps.svn_support import config_keys
28 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
@@ -38,8 +40,6 b' log = logging.getLogger(__name__)'
38 class RepoSettingsVcsView(RepoAppView):
40 class RepoSettingsVcsView(RepoAppView):
39 def load_default_context(self):
41 def load_default_context(self):
40 c = self._get_local_tmpl_context()
42 c = self._get_local_tmpl_context()
41
42
43 return c
43 return c
44
44
45 def _vcs_form_defaults(self, repo_name):
45 def _vcs_form_defaults(self, repo_name):
@@ -77,6 +77,9 b' class RepoSettingsVcsView(RepoAppView):'
77 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
77 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
78 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
78 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
79
79
80 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
81 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
82
80 defaults = self._vcs_form_defaults(self.db_repo_name)
83 defaults = self._vcs_form_defaults(self.db_repo_name)
81 c.inherit_global_settings = defaults['inherit_global_settings']
84 c.inherit_global_settings = defaults['inherit_global_settings']
82
85
@@ -103,6 +106,8 b' class RepoSettingsVcsView(RepoAppView):'
103 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
106 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
104 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
107 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
105 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
108 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
109 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
110 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
106
111
107 defaults = self._vcs_form_defaults(self.db_repo_name)
112 defaults = self._vcs_form_defaults(self.db_repo_name)
108 c.inherit_global_settings = defaults['inherit_global_settings']
113 c.inherit_global_settings = defaults['inherit_global_settings']
@@ -19,8 +19,6 b''
19 import logging
19 import logging
20
20
21 from . import config_keys
21 from . import config_keys
22 from .events import SshKeyFileChangeEvent
23 from .subscribers import generate_ssh_authorized_keys_file_subscriber
24
22
25 from rhodecode.config.settings_maker import SettingsMaker
23 from rhodecode.config.settings_maker import SettingsMaker
26
24
@@ -42,9 +40,9 b' def _sanitize_settings_and_apply_default'
42 settings_maker.make_setting(config_keys.wrapper_cmd, '')
40 settings_maker.make_setting(config_keys.wrapper_cmd, '')
43 settings_maker.make_setting(config_keys.authorized_keys_line_ssh_opts, '')
41 settings_maker.make_setting(config_keys.authorized_keys_line_ssh_opts, '')
44
42
45 settings_maker.make_setting(config_keys.ssh_hg_bin, '~/.rccontrol/vcsserver-1/profile/bin/hg')
43 settings_maker.make_setting(config_keys.ssh_hg_bin, '/usr/local/bin/rhodecode_bin/vcs_bin/hg')
46 settings_maker.make_setting(config_keys.ssh_git_bin, '~/.rccontrol/vcsserver-1/profile/bin/git')
44 settings_maker.make_setting(config_keys.ssh_git_bin, '/usr/local/bin/rhodecode_bin/vcs_bin/git')
47 settings_maker.make_setting(config_keys.ssh_svn_bin, '~/.rccontrol/vcsserver-1/profile/bin/svnserve')
45 settings_maker.make_setting(config_keys.ssh_svn_bin, '/usr/local/bin/rhodecode_bin/vcs_bin/svnserve')
48
46
49 settings_maker.env_expand()
47 settings_maker.env_expand()
50
48
@@ -55,5 +53,8 b' def includeme(config):'
55
53
56 # if we have enable generation of file, subscribe to event
54 # if we have enable generation of file, subscribe to event
57 if settings[config_keys.generate_authorized_keyfile]:
55 if settings[config_keys.generate_authorized_keyfile]:
56 # lazy import here for faster code reading... via sshwrapper-v2 mode
57 from .subscribers import generate_ssh_authorized_keys_file_subscriber
58 from .events import SshKeyFileChangeEvent
58 config.add_subscriber(
59 config.add_subscriber(
59 generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent)
60 generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent)
@@ -20,11 +20,10 b' import os'
20 import re
20 import re
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import configparser
24 from sqlalchemy import Table
23 from sqlalchemy import Table
25
24
25 from rhodecode.lib.api_utils import call_service_api
26 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.lib.utils2 import AttributeDict
27 from rhodecode.model.scm import ScmModel
28
27
29 from .hg import MercurialServer
28 from .hg import MercurialServer
30 from .git import GitServer
29 from .git import GitServer
@@ -38,7 +37,7 b' class SshWrapper(object):'
38 svn_cmd_pat = re.compile(r'^svnserve -t')
37 svn_cmd_pat = re.compile(r'^svnserve -t')
39
38
40 def __init__(self, command, connection_info, mode,
39 def __init__(self, command, connection_info, mode,
41 user, user_id, key_id: int, shell, ini_path: str, env):
40 user, user_id, key_id: int, shell, ini_path: str, settings, env):
42 self.command = command
41 self.command = command
43 self.connection_info = connection_info
42 self.connection_info = connection_info
44 self.mode = mode
43 self.mode = mode
@@ -48,15 +47,9 b' class SshWrapper(object):'
48 self.shell = shell
47 self.shell = shell
49 self.ini_path = ini_path
48 self.ini_path = ini_path
50 self.env = env
49 self.env = env
51
50 self.settings = settings
52 self.config = self.parse_config(ini_path)
53 self.server_impl = None
51 self.server_impl = None
54
52
55 def parse_config(self, config_path):
56 parser = configparser.ConfigParser()
57 parser.read(config_path)
58 return parser
59
60 def update_key_access_time(self, key_id):
53 def update_key_access_time(self, key_id):
61 from rhodecode.model.meta import raw_query_executor, Base
54 from rhodecode.model.meta import raw_query_executor, Base
62
55
@@ -161,6 +154,9 b' class SshWrapper(object):'
161 return vcs_type, repo_name, mode
154 return vcs_type, repo_name, mode
162
155
163 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
156 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
157 # TODO: remove this once we have .ini defined access path...
158 from rhodecode.model.scm import ScmModel
159
164 store = ScmModel().repos_path
160 store = ScmModel().repos_path
165
161
166 check_branch_perms = False
162 check_branch_perms = False
@@ -185,7 +181,7 b' class SshWrapper(object):'
185 server = MercurialServer(
181 server = MercurialServer(
186 store=store, ini_path=self.ini_path,
182 store=store, ini_path=self.ini_path,
187 repo_name=repo, user=user,
183 repo_name=repo, user=user,
188 user_permissions=permissions, config=self.config, env=self.env)
184 user_permissions=permissions, settings=self.settings, env=self.env)
189 self.server_impl = server
185 self.server_impl = server
190 return server.run(tunnel_extras=extras)
186 return server.run(tunnel_extras=extras)
191
187
@@ -193,7 +189,7 b' class SshWrapper(object):'
193 server = GitServer(
189 server = GitServer(
194 store=store, ini_path=self.ini_path,
190 store=store, ini_path=self.ini_path,
195 repo_name=repo, repo_mode=mode, user=user,
191 repo_name=repo, repo_mode=mode, user=user,
196 user_permissions=permissions, config=self.config, env=self.env)
192 user_permissions=permissions, settings=self.settings, env=self.env)
197 self.server_impl = server
193 self.server_impl = server
198 return server.run(tunnel_extras=extras)
194 return server.run(tunnel_extras=extras)
199
195
@@ -201,7 +197,7 b' class SshWrapper(object):'
201 server = SubversionServer(
197 server = SubversionServer(
202 store=store, ini_path=self.ini_path,
198 store=store, ini_path=self.ini_path,
203 repo_name=None, user=user,
199 repo_name=None, user=user,
204 user_permissions=permissions, config=self.config, env=self.env)
200 user_permissions=permissions, settings=self.settings, env=self.env)
205 self.server_impl = server
201 self.server_impl = server
206 return server.run(tunnel_extras=extras)
202 return server.run(tunnel_extras=extras)
207
203
@@ -261,3 +257,131 b' class SshWrapper(object):'
261 exit_code = -1
257 exit_code = -1
262
258
263 return exit_code
259 return exit_code
260
261
262 class SshWrapperStandalone(SshWrapper):
263 """
264 New version of SshWrapper designed to be depended only on service API
265 """
266 repos_path = None
267
268 @staticmethod
269 def parse_user_related_data(user_data):
270 user = AttributeDict()
271 user.user_id = user_data['user_id']
272 user.username = user_data['username']
273 user.repo_permissions = user_data['repo_permissions']
274 user.branch_permissions = user_data['branch_permissions']
275 return user
276
277 def wrap(self):
278 mode = self.mode
279 username = self.username
280 user_id = self.user_id
281 shell = self.shell
282
283 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
284
285 log.debug(
286 'Mode: `%s` User: `name:%s : id:%s` Shell: `%s` SSH Command: `\"%s\"` '
287 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
288 mode, username, user_id, shell, self.command,
289 scm_detected, scm_mode, scm_repo)
290
291 log.debug('SSH Connection info %s', self.get_connection_info())
292
293 if shell and self.command is None:
294 log.info('Dropping to shell, no command given and shell is allowed')
295 os.execl('/bin/bash', '-l')
296 exit_code = 1
297
298 elif scm_detected:
299 data = call_service_api(self.settings, {
300 "method": "service_get_data_for_ssh_wrapper",
301 "args": {"user_id": user_id, "repo_name": scm_repo, "key_id": self.key_id}
302 })
303 user = self.parse_user_related_data(data)
304 if not user:
305 log.warning('User with id %s not found', user_id)
306 exit_code = -1
307 return exit_code
308 self.repos_path = data['repos_path']
309 permissions = user.repo_permissions
310 repo_branch_permissions = user.branch_permissions
311 try:
312 exit_code, is_updated = self.serve(
313 scm_detected, scm_repo, scm_mode, user, permissions,
314 repo_branch_permissions)
315 except Exception:
316 log.exception('Error occurred during execution of SshWrapper')
317 exit_code = -1
318
319 elif self.command is None and shell is False:
320 log.error('No Command given.')
321 exit_code = -1
322
323 else:
324 log.error('Unhandled Command: "%s" Aborting.', self.command)
325 exit_code = -1
326
327 return exit_code
328
329 def maybe_translate_repo_uid(self, repo_name):
330 _org_name = repo_name
331 if _org_name.startswith('_'):
332 _org_name = _org_name.split('/', 1)[0]
333
334 if repo_name.startswith('_'):
335 org_repo_name = repo_name
336 log.debug('translating UID repo %s', org_repo_name)
337 by_id_match = call_service_api(self.settings, {
338 'method': 'service_get_repo_name_by_id',
339 "args": {"repo_id": repo_name}
340 })
341 if by_id_match:
342 repo_name = by_id_match['repo_name']
343 log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name)
344
345 return repo_name, _org_name
346
347 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
348 store = self.repos_path
349
350 check_branch_perms = False
351 detect_force_push = False
352
353 if branch_permissions:
354 check_branch_perms = True
355 detect_force_push = True
356
357 log.debug(
358 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
359 vcs, mode, repo, check_branch_perms)
360
361 # detect if we have to check branch permissions
362 extras = {
363 'detect_force_push': detect_force_push,
364 'check_branch_perms': check_branch_perms,
365 'config': self.ini_path
366 }
367
368 match vcs:
369 case 'hg':
370 server = MercurialServer(
371 store=store, ini_path=self.ini_path,
372 repo_name=repo, user=user,
373 user_permissions=permissions, settings=self.settings, env=self.env)
374 case 'git':
375 server = GitServer(
376 store=store, ini_path=self.ini_path,
377 repo_name=repo, repo_mode=mode, user=user,
378 user_permissions=permissions, settings=self.settings, env=self.env)
379 case 'svn':
380 server = SubversionServer(
381 store=store, ini_path=self.ini_path,
382 repo_name=None, user=user,
383 user_permissions=permissions, settings=self.settings, env=self.env)
384 case _:
385 raise Exception(f'Unrecognised VCS: {vcs}')
386 self.server_impl = server
387 return server.run(tunnel_extras=extras)
@@ -20,26 +20,27 b' import os'
20 import sys
20 import sys
21 import logging
21 import logging
22
22
23 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
23 from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
24 from rhodecode.lib.ext_json import sjson as json
24 from rhodecode.lib.ext_json import sjson as json
25 from rhodecode.lib.vcs.conf import settings as vcs_settings
25 from rhodecode.lib.vcs.conf import settings as vcs_settings
26 from rhodecode.model.scm import ScmModel
26 from rhodecode.lib.api_utils import call_service_api
27
27
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30
30
31 class VcsServer(object):
31 class SshVcsServer(object):
32 repo_user_agent = None # set in child classes
32 repo_user_agent = None # set in child classes
33 _path = None # set executable path for hg/git/svn binary
33 _path = None # set executable path for hg/git/svn binary
34 backend = None # set in child classes
34 backend = None # set in child classes
35 tunnel = None # subprocess handling tunnel
35 tunnel = None # subprocess handling tunnel
36 settings = None # parsed settings module
36 write_perms = ['repository.admin', 'repository.write']
37 write_perms = ['repository.admin', 'repository.write']
37 read_perms = ['repository.read', 'repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
38
39
39 def __init__(self, user, user_permissions, config, env):
40 def __init__(self, user, user_permissions, settings, env):
40 self.user = user
41 self.user = user
41 self.user_permissions = user_permissions
42 self.user_permissions = user_permissions
42 self.config = config
43 self.settings = settings
43 self.env = env
44 self.env = env
44 self.stdin = sys.stdin
45 self.stdin = sys.stdin
45
46
@@ -47,6 +48,7 b' class VcsServer(object):'
47 self.repo_mode = None
48 self.repo_mode = None
48 self.store = ''
49 self.store = ''
49 self.ini_path = ''
50 self.ini_path = ''
51 self.hooks_protocol = None
50
52
51 def _invalidate_cache(self, repo_name):
53 def _invalidate_cache(self, repo_name):
52 """
54 """
@@ -54,7 +56,16 b' class VcsServer(object):'
54
56
55 :param repo_name: full repo name, also a cache key
57 :param repo_name: full repo name, also a cache key
56 """
58 """
59 # Todo: Leave only "celery" case after transition.
60 match self.hooks_protocol:
61 case 'http':
62 from rhodecode.model.scm import ScmModel
57 ScmModel().mark_for_invalidation(repo_name)
63 ScmModel().mark_for_invalidation(repo_name)
64 case 'celery':
65 call_service_api(self.settings, {
66 "method": "service_mark_for_invalidation",
67 "args": {"repo_name": repo_name}
68 })
58
69
59 def has_write_perm(self):
70 def has_write_perm(self):
60 permission = self.user_permissions.get(self.repo_name)
71 permission = self.user_permissions.get(self.repo_name)
@@ -65,30 +76,31 b' class VcsServer(object):'
65
76
66 def _check_permissions(self, action):
77 def _check_permissions(self, action):
67 permission = self.user_permissions.get(self.repo_name)
78 permission = self.user_permissions.get(self.repo_name)
79 user_info = f'{self.user["user_id"]}:{self.user["username"]}'
68 log.debug('permission for %s on %s are: %s',
80 log.debug('permission for %s on %s are: %s',
69 self.user, self.repo_name, permission)
81 user_info, self.repo_name, permission)
70
82
71 if not permission:
83 if not permission:
72 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
84 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
73 self.user, self.repo_name)
85 user_info, self.repo_name)
74 return -2
86 return -2
75
87
76 if action == 'pull':
88 if action == 'pull':
77 if permission in self.read_perms:
89 if permission in self.read_perms:
78 log.info(
90 log.info(
79 'READ Permissions for User "%s" detected to repo "%s"!',
91 'READ Permissions for User "%s" detected to repo "%s"!',
80 self.user, self.repo_name)
92 user_info, self.repo_name)
81 return 0
93 return 0
82 else:
94 else:
83 if permission in self.write_perms:
95 if permission in self.write_perms:
84 log.info(
96 log.info(
85 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
97 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
86 self.user, self.repo_name)
98 user_info, self.repo_name)
87 return 0
99 return 0
88
100
89 log.error('Cannot properly fetch or verify user `%s` permissions. '
101 log.error('Cannot properly fetch or verify user `%s` permissions. '
90 'Permissions: %s, vcs action: %s',
102 'Permissions: %s, vcs action: %s',
91 self.user, permission, action)
103 user_info, permission, action)
92 return -2
104 return -2
93
105
94 def update_environment(self, action, extras=None):
106 def update_environment(self, action, extras=None):
@@ -107,7 +119,7 b' class VcsServer(object):'
107 'server_url': None,
119 'server_url': None,
108 'user_agent': f'{self.repo_user_agent}/ssh-user-agent',
120 'user_agent': f'{self.repo_user_agent}/ssh-user-agent',
109 'hooks': ['push', 'pull'],
121 'hooks': ['push', 'pull'],
110 'hooks_module': 'rhodecode.lib.hooks_daemon',
122 'hooks_module': 'rhodecode.lib.hook_daemon.hook_module',
111 'is_shadow_repo': False,
123 'is_shadow_repo': False,
112 'detect_force_push': False,
124 'detect_force_push': False,
113 'check_branch_perms': False,
125 'check_branch_perms': False,
@@ -134,7 +146,8 b' class VcsServer(object):'
134 if exit_code:
146 if exit_code:
135 return exit_code, False
147 return exit_code, False
136
148
137 req = self.env['request']
149 req = self.env.get('request')
150 if req:
138 server_url = req.host_url + req.script_name
151 server_url = req.host_url + req.script_name
139 extras['server_url'] = server_url
152 extras['server_url'] = server_url
140
153
@@ -144,12 +157,13 b' class VcsServer(object):'
144 return exit_code, action == "push"
157 return exit_code, action == "push"
145
158
146 def run(self, tunnel_extras=None):
159 def run(self, tunnel_extras=None):
160 self.hooks_protocol = self.settings['vcs.hooks.protocol']
147 tunnel_extras = tunnel_extras or {}
161 tunnel_extras = tunnel_extras or {}
148 extras = {}
162 extras = {}
149 extras.update(tunnel_extras)
163 extras.update(tunnel_extras)
150
164
151 callback_daemon, extras = prepare_callback_daemon(
165 callback_daemon, extras = prepare_callback_daemon(
152 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
166 extras, protocol=self.hooks_protocol,
153 host=vcs_settings.HOOKS_HOST)
167 host=vcs_settings.HOOKS_HOST)
154
168
155 with callback_daemon:
169 with callback_daemon:
@@ -21,7 +21,7 b' import logging'
21 import subprocess
21 import subprocess
22
22
23 from vcsserver import hooks
23 from vcsserver import hooks
24 from .base import VcsServer
24 from .base import SshVcsServer
25
25
26 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
27
27
@@ -70,19 +70,17 b' class GitTunnelWrapper(object):'
70 return result
70 return result
71
71
72
72
73 class GitServer(VcsServer):
73 class GitServer(SshVcsServer):
74 backend = 'git'
74 backend = 'git'
75 repo_user_agent = 'git'
75 repo_user_agent = 'git'
76
76
77 def __init__(self, store, ini_path, repo_name, repo_mode,
77 def __init__(self, store, ini_path, repo_name, repo_mode, user, user_permissions, settings, env):
78 user, user_permissions, config, env):
78 super().__init__(user, user_permissions, settings, env)
79 super().\
80 __init__(user, user_permissions, config, env)
81
79
82 self.store = store
80 self.store = store
83 self.ini_path = ini_path
81 self.ini_path = ini_path
84 self.repo_name = repo_name
82 self.repo_name = repo_name
85 self._path = self.git_path = config.get('app:main', 'ssh.executable.git')
83 self._path = self.git_path = settings['ssh.executable.git']
86
84
87 self.repo_mode = repo_mode
85 self.repo_mode = repo_mode
88 self.tunnel = GitTunnelWrapper(server=self)
86 self.tunnel = GitTunnelWrapper(server=self)
@@ -22,9 +22,10 b' import logging'
22 import tempfile
22 import tempfile
23 import textwrap
23 import textwrap
24 import collections
24 import collections
25 from .base import VcsServer
25
26 from rhodecode.model.db import RhodeCodeUi
26 from .base import SshVcsServer
27 from rhodecode.model.settings import VcsSettingsModel
27
28 from rhodecode.lib.api_utils import call_service_api
28
29
29 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
30
31
@@ -56,7 +57,7 b' class MercurialTunnelWrapper(object):'
56 # cleanup custom hgrc file
57 # cleanup custom hgrc file
57 if os.path.isfile(hgrc_custom):
58 if os.path.isfile(hgrc_custom):
58 with open(hgrc_custom, 'wb') as f:
59 with open(hgrc_custom, 'wb') as f:
59 f.write('')
60 f.write(b'')
60 log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
61 log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
61
62
62 # write temp
63 # write temp
@@ -93,21 +94,31 b' class MercurialTunnelWrapper(object):'
93 self.remove_configs()
94 self.remove_configs()
94
95
95
96
96 class MercurialServer(VcsServer):
97 class MercurialServer(SshVcsServer):
97 backend = 'hg'
98 backend = 'hg'
98 repo_user_agent = 'mercurial'
99 repo_user_agent = 'mercurial'
99 cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
100 cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
100
101
101 def __init__(self, store, ini_path, repo_name, user, user_permissions, config, env):
102 def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env):
102 super().__init__(user, user_permissions, config, env)
103 super().__init__(user, user_permissions, settings, env)
103
104
104 self.store = store
105 self.store = store
105 self.ini_path = ini_path
106 self.ini_path = ini_path
106 self.repo_name = repo_name
107 self.repo_name = repo_name
107 self._path = self.hg_path = config.get('app:main', 'ssh.executable.hg')
108 self._path = self.hg_path = settings['ssh.executable.hg']
108 self.tunnel = MercurialTunnelWrapper(server=self)
109 self.tunnel = MercurialTunnelWrapper(server=self)
109
110
110 def config_to_hgrc(self, repo_name):
111 def config_to_hgrc(self, repo_name):
112 # Todo: once transition is done only call to service api should exist
113 if self.hooks_protocol == 'celery':
114 data = call_service_api(self.settings, {
115 "method": "service_config_to_hgrc",
116 "args": {"cli_flags": self.cli_flags, "repo_name": repo_name}
117 })
118 return data['flags']
119 else:
120 from rhodecode.model.db import RhodeCodeUi
121 from rhodecode.model.settings import VcsSettingsModel
111 ui_sections = collections.defaultdict(list)
122 ui_sections = collections.defaultdict(list)
112 ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None)
123 ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None)
113
124
@@ -25,7 +25,8 b' import tempfile'
25 from subprocess import Popen, PIPE
25 from subprocess import Popen, PIPE
26 import urllib.parse
26 import urllib.parse
27
27
28 from .base import VcsServer
28 from rhodecode_tools.lib.utils import safe_str
29 from .base import SshVcsServer
29
30
30 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
31
32
@@ -81,7 +82,7 b' class SubversionTunnelWrapper(object):'
81
82
82 def sync(self):
83 def sync(self):
83 while self.process.poll() is None:
84 while self.process.poll() is None:
84 next_byte = self.stdin.read(1)
85 next_byte = self.stdin.buffer.read(1)
85 if not next_byte:
86 if not next_byte:
86 break
87 break
87 self.process.stdin.write(next_byte)
88 self.process.stdin.write(next_byte)
@@ -101,19 +102,27 b' class SubversionTunnelWrapper(object):'
101
102
102 def patch_first_client_response(self, response, **kwargs):
103 def patch_first_client_response(self, response, **kwargs):
103 self.create_hooks_env()
104 self.create_hooks_env()
104 data = response.copy()
105
105 data.update(kwargs)
106 version = response['version']
106 data['url'] = self._svn_string(data['url'])
107 capabilities = response['capabilities']
107 data['ra_client'] = self._svn_string(data['ra_client'])
108 client = response['client'] or b''
108 data['client'] = data['client'] or ''
109
109 buffer_ = (
110 url = self._svn_bytes(response['url'])
110 "( {version} ( {capabilities} ) {url}{ra_client}"
111 ra_client = self._svn_bytes(response['ra_client'])
111 "( {client}) ) ".format(**data))
112
113 buffer_ = b"( %b ( %b ) %b%b( %b) ) " % (
114 version,
115 capabilities,
116 url,
117 ra_client,
118 client
119 )
112 self.process.stdin.write(buffer_)
120 self.process.stdin.write(buffer_)
113
121
114 def fail(self, message):
122 def fail(self, message):
115 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
123 fail_msg = b"( failure ( ( 210005 %b 0: 0 ) ) )" % self._svn_bytes(message)
116 message=self._svn_string(message)))
124 sys.stdout.buffer.write(fail_msg)
125 sys.stdout.flush()
117 self.remove_configs()
126 self.remove_configs()
118 self.process.kill()
127 self.process.kill()
119 return 1
128 return 1
@@ -121,27 +130,28 b' class SubversionTunnelWrapper(object):'
121 def interrupt(self, signum, frame):
130 def interrupt(self, signum, frame):
122 self.fail("Exited by timeout")
131 self.fail("Exited by timeout")
123
132
124 def _svn_string(self, str_):
133 def _svn_bytes(self, bytes_: bytes) -> bytes:
125 if not str_:
134 if not bytes_:
126 return ''
135 return b''
127 return f'{len(str_)}:{str_} '
136
137 return f'{len(bytes_)}:'.encode() + bytes_ + b' '
128
138
129 def _read_first_client_response(self):
139 def _read_first_client_response(self):
130 buffer_ = ""
140 buffer_ = b""
131 brackets_stack = []
141 brackets_stack = []
132 while True:
142 while True:
133 next_byte = self.stdin.read(1)
143 next_byte = self.stdin.buffer.read(1)
134 buffer_ += next_byte
144 buffer_ += next_byte
135 if next_byte == "(":
145 if next_byte == b"(":
136 brackets_stack.append(next_byte)
146 brackets_stack.append(next_byte)
137 elif next_byte == ")":
147 elif next_byte == b")":
138 brackets_stack.pop()
148 brackets_stack.pop()
139 elif next_byte == " " and not brackets_stack:
149 elif next_byte == b" " and not brackets_stack:
140 break
150 break
141
151
142 return buffer_
152 return buffer_
143
153
144 def _parse_first_client_response(self, buffer_):
154 def _parse_first_client_response(self, buffer_: bytes):
145 """
155 """
146 According to the Subversion RA protocol, the first request
156 According to the Subversion RA protocol, the first request
147 should look like:
157 should look like:
@@ -151,16 +161,20 b' class SubversionTunnelWrapper(object):'
151
161
152 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
162 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
153 """
163 """
154 version_re = r'(?P<version>\d+)'
164 version_re = br'(?P<version>\d+)'
155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
165 capabilities_re = br'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
156 url_re = r'\d+\:(?P<url>[\W\w]+)'
166 url_re = br'\d+\:(?P<url>[\W\w]+)'
157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
167 ra_client_re = br'(\d+\:(?P<ra_client>[\W\w]+)\s)'
158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
168 client_re = br'(\d+\:(?P<client>[\W\w]+)\s)*'
159 regex = re.compile(
169 regex = re.compile(
160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
170 br'^\(\s%b\s%b\s%b\s%b'
161 r'\(\s{client}\)\s\)\s*$'.format(
171 br'\(\s%b\)\s\)\s*$' % (
162 version=version_re, capabilities=capabilities_re,
172 version_re,
163 url=url_re, ra_client=ra_client_re, client=client_re))
173 capabilities_re,
174 url_re,
175 ra_client_re,
176 client_re)
177 )
164 matcher = regex.match(buffer_)
178 matcher = regex.match(buffer_)
165
179
166 return matcher.groupdict() if matcher else None
180 return matcher.groupdict() if matcher else None
@@ -198,11 +212,11 b' class SubversionTunnelWrapper(object):'
198
212
199 first_response = self.get_first_client_response()
213 first_response = self.get_first_client_response()
200 if not first_response:
214 if not first_response:
201 return self.fail("Repository name cannot be extracted")
215 return self.fail(b"Repository name cannot be extracted")
202
216
203 url_parts = urllib.parse.urlparse(first_response['url'])
217 url_parts = urllib.parse.urlparse(first_response['url'])
204
218
205 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
219 self.server.repo_name = self._match_repo_name(safe_str(url_parts.path).strip('/'))
206
220
207 exit_code = self.server._check_permissions(action)
221 exit_code = self.server._check_permissions(action)
208 if exit_code:
222 if exit_code:
@@ -218,20 +232,18 b' class SubversionTunnelWrapper(object):'
218 return self.return_code
232 return self.return_code
219
233
220
234
221 class SubversionServer(VcsServer):
235 class SubversionServer(SshVcsServer):
222 backend = 'svn'
236 backend = 'svn'
223 repo_user_agent = 'svn'
237 repo_user_agent = 'svn'
224
238
225 def __init__(self, store, ini_path, repo_name,
239 def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env):
226 user, user_permissions, config, env):
240 super().__init__(user, user_permissions, settings, env)
227 super()\
228 .__init__(user, user_permissions, config, env)
229 self.store = store
241 self.store = store
230 self.ini_path = ini_path
242 self.ini_path = ini_path
231 # NOTE(dan): repo_name at this point is empty,
243 # NOTE(dan): repo_name at this point is empty,
232 # this is set later in .run() based from parsed input stream
244 # this is set later in .run() based from parsed input stream
233 self.repo_name = repo_name
245 self.repo_name = repo_name
234 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
246 self._path = self.svn_path = settings['ssh.executable.svn']
235
247
236 self.tunnel = SubversionTunnelWrapper(server=self)
248 self.tunnel = SubversionTunnelWrapper(server=self)
237
249
@@ -244,7 +256,8 b' class SubversionServer(VcsServer):'
244 # if exit_code:
256 # if exit_code:
245 # return exit_code, False
257 # return exit_code, False
246
258
247 req = self.env['request']
259 req = self.env.get('request')
260 if req:
248 server_url = req.host_url + req.script_name
261 server_url = req.host_url + req.script_name
249 extras['server_url'] = server_url
262 extras['server_url'] = server_url
250
263
@@ -23,29 +23,14 b' import logging'
23
23
24 import click
24 import click
25
25
26 from pyramid.paster import setup_logging
27
28 from rhodecode.lib.pyramid_utils import bootstrap
26 from rhodecode.lib.pyramid_utils import bootstrap
29 from rhodecode.lib.statsd_client import StatsdClient
27 from rhodecode.lib.statsd_client import StatsdClient
30 from .backends import SshWrapper
28 from .backends import SshWrapper
29 from .utils import setup_custom_logging
31
30
32 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
33
32
34
33
35 def setup_custom_logging(ini_path, debug):
36 if debug:
37 # enabled rhodecode.ini controlled logging setup
38 setup_logging(ini_path)
39 else:
40 # configure logging in a mode that doesn't print anything.
41 # in case of regularly configured logging it gets printed out back
42 # to the client doing an SSH command.
43 logger = logging.getLogger('')
44 null = logging.NullHandler()
45 # add the handler to the root logger
46 logger.handlers = [null]
47
48
49 @click.command()
34 @click.command()
50 @click.argument('ini_path', type=click.Path(exists=True))
35 @click.argument('ini_path', type=click.Path(exists=True))
51 @click.option(
36 @click.option(
@@ -69,11 +54,12 b' def main(ini_path, mode, user, user_id, '
69 connection_info = os.environ.get('SSH_CONNECTION', '')
54 connection_info = os.environ.get('SSH_CONNECTION', '')
70 time_start = time.time()
55 time_start = time.time()
71 with bootstrap(ini_path, env={'RC_CMD_SSH_WRAPPER': '1'}) as env:
56 with bootstrap(ini_path, env={'RC_CMD_SSH_WRAPPER': '1'}) as env:
57 settings = env['registry'].settings
72 statsd = StatsdClient.statsd
58 statsd = StatsdClient.statsd
73 try:
59 try:
74 ssh_wrapper = SshWrapper(
60 ssh_wrapper = SshWrapper(
75 command, connection_info, mode,
61 command, connection_info, mode,
76 user, user_id, key_id, shell, ini_path, env)
62 user, user_id, key_id, shell, ini_path, settings, env)
77 except Exception:
63 except Exception:
78 log.exception('Failed to execute SshWrapper')
64 log.exception('Failed to execute SshWrapper')
79 sys.exit(-5)
65 sys.exit(-5)
@@ -20,7 +20,7 b' import os'
20 import pytest
20 import pytest
21 import configparser
21 import configparser
22
22
23 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper
23 from rhodecode.apps.ssh_support.lib.ssh_wrapper_v1 import SshWrapper
24 from rhodecode.lib.utils2 import AttributeDict
24 from rhodecode.lib.utils2 import AttributeDict
25
25
26
26
@@ -52,7 +52,10 b' def dummy_env():'
52
52
53
53
54 def plain_dummy_user():
54 def plain_dummy_user():
55 return AttributeDict(username='test_user')
55 return AttributeDict(
56 user_id=1,
57 username='test_user'
58 )
56
59
57
60
58 @pytest.fixture()
61 @pytest.fixture()
@@ -65,4 +68,4 b' def ssh_wrapper(app, dummy_conf_file, du'
65 conn_info = '127.0.0.1 22 10.0.0.1 443'
68 conn_info = '127.0.0.1 22 10.0.0.1 443'
66 return SshWrapper(
69 return SshWrapper(
67 'random command', conn_info, 'auto', 'admin', '1', key_id='1',
70 'random command', conn_info, 'auto', 'admin', '1', key_id='1',
68 shell=False, ini_path=dummy_conf_file, env=dummy_env)
71 shell=False, ini_path=dummy_conf_file, settings={}, env=dummy_env)
@@ -25,6 +25,7 b' from rhodecode.apps.ssh_support.lib.back'
25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
26 from rhodecode.lib.ext_json import json
26 from rhodecode.lib.ext_json import json
27
27
28
28 class GitServerCreator(object):
29 class GitServerCreator(object):
29 root = '/tmp/repo/path/'
30 root = '/tmp/repo/path/'
30 git_path = '/usr/local/bin/git'
31 git_path = '/usr/local/bin/git'
@@ -39,10 +40,7 b' class GitServerCreator(object):'
39 user = plain_dummy_user()
40 user = plain_dummy_user()
40
41
41 def __init__(self):
42 def __init__(self):
42 def config_get(part, key):
43 pass
43 return self.config_data.get(part, {}).get(key)
44 self.config_mock = mock.Mock()
45 self.config_mock.get = mock.Mock(side_effect=config_get)
46
44
47 def create(self, **kwargs):
45 def create(self, **kwargs):
48 parameters = {
46 parameters = {
@@ -54,7 +52,7 b' class GitServerCreator(object):'
54 'user_permissions': {
52 'user_permissions': {
55 self.repo_name: 'repository.admin'
53 self.repo_name: 'repository.admin'
56 },
54 },
57 'config': self.config_mock,
55 'settings': self.config_data['app:main'],
58 'env': plain_dummy_env()
56 'env': plain_dummy_env()
59 }
57 }
60 parameters.update(kwargs)
58 parameters.update(kwargs)
@@ -142,7 +140,7 b' class TestGitServer(object):'
142 'server_url': None,
140 'server_url': None,
143 'hooks': ['push', 'pull'],
141 'hooks': ['push', 'pull'],
144 'is_shadow_repo': False,
142 'is_shadow_repo': False,
145 'hooks_module': 'rhodecode.lib.hooks_daemon',
143 'hooks_module': 'rhodecode.lib.hook_daemon.hook_module',
146 'check_branch_perms': False,
144 'check_branch_perms': False,
147 'detect_force_push': False,
145 'detect_force_push': False,
148 'user_agent': u'git/ssh-user-agent',
146 'user_agent': u'git/ssh-user-agent',
@@ -38,10 +38,7 b' class MercurialServerCreator(object):'
38 user = plain_dummy_user()
38 user = plain_dummy_user()
39
39
40 def __init__(self):
40 def __init__(self):
41 def config_get(part, key):
41 pass
42 return self.config_data.get(part, {}).get(key)
43 self.config_mock = mock.Mock()
44 self.config_mock.get = mock.Mock(side_effect=config_get)
45
42
46 def create(self, **kwargs):
43 def create(self, **kwargs):
47 parameters = {
44 parameters = {
@@ -52,7 +49,7 b' class MercurialServerCreator(object):'
52 'user_permissions': {
49 'user_permissions': {
53 'test_hg': 'repository.admin'
50 'test_hg': 'repository.admin'
54 },
51 },
55 'config': self.config_mock,
52 'settings': self.config_data['app:main'],
56 'env': plain_dummy_env()
53 'env': plain_dummy_env()
57 }
54 }
58 parameters.update(kwargs)
55 parameters.update(kwargs)
@@ -36,10 +36,7 b' class SubversionServerCreator(object):'
36 user = plain_dummy_user()
36 user = plain_dummy_user()
37
37
38 def __init__(self):
38 def __init__(self):
39 def config_get(part, key):
39 pass
40 return self.config_data.get(part, {}).get(key)
41 self.config_mock = mock.Mock()
42 self.config_mock.get = mock.Mock(side_effect=config_get)
43
40
44 def create(self, **kwargs):
41 def create(self, **kwargs):
45 parameters = {
42 parameters = {
@@ -50,7 +47,7 b' class SubversionServerCreator(object):'
50 'user_permissions': {
47 'user_permissions': {
51 self.repo_name: 'repository.admin'
48 self.repo_name: 'repository.admin'
52 },
49 },
53 'config': self.config_mock,
50 'settings': self.config_data['app:main'],
54 'env': plain_dummy_env()
51 'env': plain_dummy_env()
55 }
52 }
56
53
@@ -65,6 +62,7 b' def svn_server(app):'
65
62
66
63
67 class TestSubversionServer(object):
64 class TestSubversionServer(object):
65
68 def test_command(self, svn_server):
66 def test_command(self, svn_server):
69 server = svn_server.create()
67 server = svn_server.create()
70 expected_command = [
68 expected_command = [
@@ -28,10 +28,6 b' class TestSSHWrapper(object):'
28 permissions={}, branch_permissions={})
28 permissions={}, branch_permissions={})
29 assert str(exc_info.value) == 'Unrecognised VCS: microsoft-tfs'
29 assert str(exc_info.value) == 'Unrecognised VCS: microsoft-tfs'
30
30
31 def test_parse_config(self, ssh_wrapper):
32 config = ssh_wrapper.parse_config(ssh_wrapper.ini_path)
33 assert config
34
35 def test_get_connection_info(self, ssh_wrapper):
31 def test_get_connection_info(self, ssh_wrapper):
36 conn_info = ssh_wrapper.get_connection_info()
32 conn_info = ssh_wrapper.get_connection_info()
37 assert {'client_ip': '127.0.0.1',
33 assert {'client_ip': '127.0.0.1',
@@ -22,7 +22,7 b' import os'
22 from pyramid.renderers import render
22 from pyramid.renderers import render
23
23
24 from rhodecode.events import trigger
24 from rhodecode.events import trigger
25 from rhodecode.lib.utils import get_rhodecode_realm, get_rhodecode_base_path
25 from rhodecode.lib.utils import get_rhodecode_realm, get_rhodecode_repo_store_path
26 from rhodecode.lib.utils2 import str2bool
26 from rhodecode.lib.utils2 import str2bool
27 from rhodecode.model.db import RepoGroup
27 from rhodecode.model.db import RepoGroup
28
28
@@ -38,7 +38,7 b' def write_mod_dav_svn_config(settings):'
38 file_path = settings[config_keys.config_file_path]
38 file_path = settings[config_keys.config_file_path]
39 config = _render_mod_dav_svn_config(
39 config = _render_mod_dav_svn_config(
40 use_ssl=use_ssl,
40 use_ssl=use_ssl,
41 parent_path_root=get_rhodecode_base_path(),
41 parent_path_root=get_rhodecode_repo_store_path(),
42 list_parent_path=settings[config_keys.list_parent_path],
42 list_parent_path=settings[config_keys.list_parent_path],
43 location_root=settings[config_keys.location_root],
43 location_root=settings[config_keys.location_root],
44 repo_groups=RepoGroup.get_all_repo_groups(),
44 repo_groups=RepoGroup.get_all_repo_groups(),
@@ -389,11 +389,7 b' class RhodeCodeAuthPluginBase(object):'
389 log.debug(
389 log.debug(
390 'Trying to fetch user `%s` from RhodeCode database', username)
390 'Trying to fetch user `%s` from RhodeCode database', username)
391 if username:
391 if username:
392 user = User.get_by_username(username)
392 user = User.get_by_username_or_primary_email(username)
393 if not user:
394 log.debug('User not found, fallback to fetch user in '
395 'case insensitive mode')
396 user = User.get_by_username(username, case_insensitive=True)
397 else:
393 else:
398 log.debug('provided username:`%s` is empty skipping...', username)
394 log.debug('provided username:`%s` is empty skipping...', username)
399 if not user:
395 if not user:
@@ -31,7 +31,7 b' import urllib.parse'
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.ext_json import json, formatted_json
37 from rhodecode.lib.ext_json import json, formatted_json
@@ -53,7 +53,7 b' class CrowdAuthnResource(AuthnPluginReso'
53 pass
53 pass
54
54
55
55
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class CrowdSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
57 host = colander.SchemaNode(
57 host = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='127.0.0.1',
59 default='127.0.0.1',
@@ -33,7 +33,7 b' import urllib.error'
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39 from rhodecode.model.db import User
39 from rhodecode.model.db import User
@@ -55,7 +55,7 b' class JasigCasAuthnResource(AuthnPluginR'
55 pass
55 pass
56
56
57
57
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
58 class JasigCasSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
59 service_url = colander.SchemaNode(
59 service_url = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 default='https://domain.com/cas/v1/tickets',
61 default='https://domain.com/cas/v1/tickets',
@@ -27,7 +27,7 b' import colander'
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.authentication.base import (
28 from rhodecode.authentication.base import (
29 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
29 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.lib.colander_utils import strip_whitespace
32 from rhodecode.lib.colander_utils import strip_whitespace
33 from rhodecode.lib.exceptions import (
33 from rhodecode.lib.exceptions import (
@@ -245,7 +245,7 b' class AuthLdap(AuthLdapBase):'
245 return dn, user_attrs
245 return dn, user_attrs
246
246
247
247
248 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
248 class LdapSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
249 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
249 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
250 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
250 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
251 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
251 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
@@ -31,7 +31,7 b' import socket'
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37
37
@@ -51,7 +51,7 b' class PamAuthnResource(AuthnPluginResour'
51 pass
51 pass
52
52
53
53
54 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
54 class PamSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
55 service = colander.SchemaNode(
55 service = colander.SchemaNode(
56 colander.String(),
56 colander.String(),
57 default='login',
57 default='login',
@@ -27,7 +27,7 b' import colander'
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.lib.utils2 import safe_bytes
28 from rhodecode.lib.utils2 import safe_bytes
29 from rhodecode.model.db import User
29 from rhodecode.model.db import User
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
31 from rhodecode.authentication.base import (
31 from rhodecode.authentication.base import (
32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
@@ -169,7 +169,7 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
170 return user_attrs
170 return user_attrs
171
171
172 elif userobj.username == username and password_match:
172 elif (userobj.username == username or userobj.email == username) and password_match:
173 log.info('user `%s` authenticated correctly', userobj.username,
173 log.info('user `%s` authenticated correctly', userobj.username,
174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
175 return user_attrs
175 return user_attrs
@@ -182,8 +182,7 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
182 return None
182 return None
183
183
184
184
185 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
185 class RhodeCodeSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
186
187 auth_restriction_choices = [
186 auth_restriction_choices = [
188 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
187 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
189 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
188 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
@@ -48,3 +48,17 b' class AuthnPluginSettingsSchemaBase(cola'
48 validator=colander.Range(min=0, max=None),
48 validator=colander.Range(min=0, max=None),
49 widget='int',
49 widget='int',
50 )
50 )
51
52
53 class TwoFactorAuthnPluginSettingsSchemaMixin(colander.MappingSchema):
54 """
55 Mixin for extending plugins with two-factor authentication option.
56 """
57 global_2fa = colander.SchemaNode(
58 colander.Bool(),
59 default=False,
60 description=_('Force all users to use two factor authentication with this plugin.'),
61 missing=False,
62 title=_('enforce 2FA for users'),
63 widget='bool',
64 )
@@ -46,8 +46,7 b' def load_pyramid_environment(global_conf'
46 # If this is a test run we prepare the test environment like
46 # If this is a test run we prepare the test environment like
47 # creating a test database, test search index and test repositories.
47 # creating a test database, test search index and test repositories.
48 # This has to be done before the database connection is initialized.
48 # This has to be done before the database connection is initialized.
49 if settings['is_test']:
49 if rhodecode.is_test:
50 rhodecode.is_test = True
51 rhodecode.disable_error_handler = True
50 rhodecode.disable_error_handler = True
52 from rhodecode import authentication
51 from rhodecode import authentication
53 authentication.plugin_default_auth_ttl = 0
52 authentication.plugin_default_auth_ttl = 0
@@ -81,7 +80,6 b' def load_pyramid_environment(global_conf'
81 rhodecode.PYRAMID_SETTINGS = settings_merged
80 rhodecode.PYRAMID_SETTINGS = settings_merged
82 rhodecode.CONFIG = settings_merged
81 rhodecode.CONFIG = settings_merged
83 rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id()
82 rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id()
84 rhodecode.CONFIG['default_base_path'] = utils.get_default_base_path()
85
83
86 if vcs_server_enabled:
84 if vcs_server_enabled:
87 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
85 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
@@ -19,14 +19,13 b''
19 import os
19 import os
20 import sys
20 import sys
21 import collections
21 import collections
22 import tempfile
22
23 import time
23 import time
24 import logging.config
24 import logging.config
25
25
26 from paste.gzipper import make_gzip_middleware
26 from paste.gzipper import make_gzip_middleware
27 import pyramid.events
27 import pyramid.events
28 from pyramid.wsgi import wsgiapp
28 from pyramid.wsgi import wsgiapp
29 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.config import Configurator
29 from pyramid.config import Configurator
31 from pyramid.settings import asbool, aslist
30 from pyramid.settings import asbool, aslist
32 from pyramid.httpexceptions import (
31 from pyramid.httpexceptions import (
@@ -35,11 +34,11 b' from pyramid.renderers import render_to_'
35
34
36 from rhodecode.model import meta
35 from rhodecode.model import meta
37 from rhodecode.config import patches
36 from rhodecode.config import patches
38 from rhodecode.config import utils as config_utils
37
39 from rhodecode.config.settings_maker import SettingsMaker
40 from rhodecode.config.environment import load_pyramid_environment
38 from rhodecode.config.environment import load_pyramid_environment
41
39
42 import rhodecode.events
40 import rhodecode.events
41 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
43 from rhodecode.lib.middleware.vcs import VCSMiddleware
42 from rhodecode.lib.middleware.vcs import VCSMiddleware
44 from rhodecode.lib.request import Request
43 from rhodecode.lib.request import Request
45 from rhodecode.lib.vcs import VCSCommunicationError
44 from rhodecode.lib.vcs import VCSCommunicationError
@@ -327,7 +326,7 b' def includeme(config, auth_resources=Non'
327 config.include('pyramid_mako')
326 config.include('pyramid_mako')
328 config.include('rhodecode.lib.rc_beaker')
327 config.include('rhodecode.lib.rc_beaker')
329 config.include('rhodecode.lib.rc_cache')
328 config.include('rhodecode.lib.rc_cache')
330 config.include('rhodecode.lib.rc_cache.archive_cache')
329 config.include('rhodecode.lib.archive_cache')
331
330
332 config.include('rhodecode.apps._base.navigation')
331 config.include('rhodecode.apps._base.navigation')
333 config.include('rhodecode.apps._base.subscribers')
332 config.include('rhodecode.apps._base.subscribers')
@@ -465,173 +464,3 b' def wrap_app_in_wsgi_middlewares(pyramid'
465 log.debug('Request processing finalized: %.4fs', total)
464 log.debug('Request processing finalized: %.4fs', total)
466
465
467 return pyramid_app_with_cleanup
466 return pyramid_app_with_cleanup
468
469
470 def sanitize_settings_and_apply_defaults(global_config, settings):
471 """
472 Applies settings defaults and does all type conversion.
473
474 We would move all settings parsing and preparation into this place, so that
475 we have only one place left which deals with this part. The remaining parts
476 of the application would start to rely fully on well prepared settings.
477
478 This piece would later be split up per topic to avoid a big fat monster
479 function.
480 """
481 jn = os.path.join
482
483 global_settings_maker = SettingsMaker(global_config)
484 global_settings_maker.make_setting('debug', default=False, parser='bool')
485 debug_enabled = asbool(global_config.get('debug'))
486
487 settings_maker = SettingsMaker(settings)
488
489 settings_maker.make_setting(
490 'logging.autoconfigure',
491 default=False,
492 parser='bool')
493
494 logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini')
495 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
496
497 # Default includes, possible to change as a user
498 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
499 log.debug(
500 "Using the following pyramid.includes: %s",
501 pyramid_includes)
502
503 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
504 settings_maker.make_setting('rhodecode.edition_id', 'CE')
505
506 if 'mako.default_filters' not in settings:
507 # set custom default filters if we don't have it defined
508 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
509 settings['mako.default_filters'] = 'h_filter'
510
511 if 'mako.directories' not in settings:
512 mako_directories = settings.setdefault('mako.directories', [
513 # Base templates of the original application
514 'rhodecode:templates',
515 ])
516 log.debug(
517 "Using the following Mako template directories: %s",
518 mako_directories)
519
520 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
521 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
522 raw_url = settings['beaker.session.url']
523 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
524 settings['beaker.session.url'] = 'redis://' + raw_url
525
526 settings_maker.make_setting('__file__', global_config.get('__file__'))
527
528 # TODO: johbo: Re-think this, usually the call to config.include
529 # should allow to pass in a prefix.
530 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
531
532 # Sanitize generic settings.
533 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
534 settings_maker.make_setting('is_test', False, parser='bool')
535 settings_maker.make_setting('gzip_responses', False, parser='bool')
536
537 # statsd
538 settings_maker.make_setting('statsd.enabled', False, parser='bool')
539 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
540 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
541 settings_maker.make_setting('statsd.statsd_prefix', '')
542 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
543
544 settings_maker.make_setting('vcs.svn.compatible_version', '')
545 settings_maker.make_setting('vcs.hooks.protocol', 'http')
546 settings_maker.make_setting('vcs.hooks.host', '*')
547 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
548 settings_maker.make_setting('vcs.server', '')
549 settings_maker.make_setting('vcs.server.protocol', 'http')
550 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
551 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
552 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
553 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
554 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
555 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
556
557 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
558
559 # Support legacy values of vcs.scm_app_implementation. Legacy
560 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
561 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
562 scm_app_impl = settings['vcs.scm_app_implementation']
563 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
564 settings['vcs.scm_app_implementation'] = 'http'
565
566 settings_maker.make_setting('appenlight', False, parser='bool')
567
568 temp_store = tempfile.gettempdir()
569 tmp_cache_dir = jn(temp_store, 'rc_cache')
570
571 # save default, cache dir, and use it for all backends later.
572 default_cache_dir = settings_maker.make_setting(
573 'cache_dir',
574 default=tmp_cache_dir, default_when_empty=True,
575 parser='dir:ensured')
576
577 # exception store cache
578 settings_maker.make_setting(
579 'exception_tracker.store_path',
580 default=jn(default_cache_dir, 'exc_store'), default_when_empty=True,
581 parser='dir:ensured'
582 )
583
584 settings_maker.make_setting(
585 'celerybeat-schedule.path',
586 default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
587 parser='file:ensured'
588 )
589
590 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
591 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
592
593 # sessions, ensure file since no-value is memory
594 settings_maker.make_setting('beaker.session.type', 'file')
595 settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data'))
596
597 # cache_general
598 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
599 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
600 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db'))
601
602 # cache_perms
603 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
604 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
605 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db'))
606
607 # cache_repo
608 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
609 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
610 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db'))
611
612 # cache_license
613 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
614 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
615 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db'))
616
617 # cache_repo_longterm memory, 96H
618 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
619 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
620 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
621
622 # sql_cache_short
623 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
624 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
625 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
626
627 # archive_cache
628 settings_maker.make_setting('archive_cache.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
629 settings_maker.make_setting('archive_cache.cache_size_gb', 10, parser='float')
630 settings_maker.make_setting('archive_cache.cache_shards', 10, parser='int')
631
632 settings_maker.env_expand()
633
634 # configure instance id
635 config_utils.set_instance_id(settings)
636
637 return settings
@@ -23,6 +23,7 b' import functools'
23 import logging
23 import logging
24 import tempfile
24 import tempfile
25 import logging.config
25 import logging.config
26
26 from rhodecode.lib.type_utils import str2bool, aslist
27 from rhodecode.lib.type_utils import str2bool, aslist
27
28
28 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
@@ -34,13 +35,16 b' set_keys = {'
34 }
35 }
35
36
36
37
37 class SettingsMaker(object):
38 class SettingsMaker:
38
39
39 def __init__(self, app_settings):
40 def __init__(self, app_settings):
40 self.settings = app_settings
41 self.settings = app_settings
41
42
42 @classmethod
43 @classmethod
43 def _bool_func(cls, input_val):
44 def _bool_func(cls, input_val):
45 if isinstance(input_val, bytes):
46 # decode to str
47 input_val = input_val.decode('utf8')
44 return str2bool(input_val)
48 return str2bool(input_val)
45
49
46 @classmethod
50 @classmethod
@@ -62,11 +66,24 b' class SettingsMaker(object):'
62 return input_val
66 return input_val
63
67
64 @classmethod
68 @classmethod
69 def _string_no_quote_func(cls, input_val, lower=True):
70 """
71 Special case string function that detects if value is set to empty quote string
72 e.g.
73
74 core.binar_dir = ""
75 """
76
77 input_val = cls._string_func(input_val, lower=lower)
78 if input_val in ['""', "''"]:
79 return ''
80
81 @classmethod
65 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
82 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
66
83
67 # ensure we have our dir created
84 # ensure we have our dir created
68 if not os.path.isdir(input_val) and ensure_dir:
85 if not os.path.isdir(input_val) and ensure_dir:
69 os.makedirs(input_val, mode=mode)
86 os.makedirs(input_val, mode=mode, exist_ok=True)
70
87
71 if not os.path.isdir(input_val):
88 if not os.path.isdir(input_val):
72 raise Exception(f'Dir at {input_val} does not exist')
89 raise Exception(f'Dir at {input_val} does not exist')
@@ -150,6 +167,7 b' class SettingsMaker(object):'
150 'list:newline': functools.partial(self._list_func, sep='/n'),
167 'list:newline': functools.partial(self._list_func, sep='/n'),
151 'list:spacesep': functools.partial(self._list_func, sep=' '),
168 'list:spacesep': functools.partial(self._list_func, sep=' '),
152 'string': functools.partial(self._string_func, lower=lower),
169 'string': functools.partial(self._string_func, lower=lower),
170 'string:noquote': functools.partial(self._string_no_quote_func, lower=lower),
153 'dir': self._dir_func,
171 'dir': self._dir_func,
154 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
172 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
155 'file': self._file_path_func,
173 'file': self._file_path_func,
@@ -19,7 +19,7 b''
19 import os
19 import os
20 import platform
20 import platform
21
21
22 from rhodecode.model import init_model
22 DEFAULT_USER = 'default'
23
23
24
24
25 def configure_vcs(config):
25 def configure_vcs(config):
@@ -44,6 +44,7 b' def configure_vcs(config):'
44
44
45 def initialize_database(config):
45 def initialize_database(config):
46 from rhodecode.lib.utils2 import engine_from_config, get_encryption_key
46 from rhodecode.lib.utils2 import engine_from_config, get_encryption_key
47 from rhodecode.model import init_model
47 engine = engine_from_config(config, 'sqlalchemy.db1.')
48 engine = engine_from_config(config, 'sqlalchemy.db1.')
48 init_model(engine, encryption_key=get_encryption_key(config))
49 init_model(engine, encryption_key=get_encryption_key(config))
49
50
@@ -93,25 +94,17 b' def set_instance_id(config):'
93
94
94
95
95 def get_default_user_id():
96 def get_default_user_id():
96 DEFAULT_USER = 'default'
97 from sqlalchemy import text
97 from sqlalchemy import text
98 from rhodecode.model import meta
98 from rhodecode.model import meta
99
99
100 engine = meta.get_engine()
100 engine = meta.get_engine()
101 with meta.SA_Session(engine) as session:
101 with meta.SA_Session(engine) as session:
102 result = session.execute(text("SELECT user_id from users where username = :uname"), {'uname': DEFAULT_USER})
102 result = session.execute(text(
103 user_id = result.first()[0]
103 "SELECT user_id from users where username = :uname"
104 ), {'uname': DEFAULT_USER})
105 user = result.first()
106 if not user:
107 raise ValueError('Unable to retrieve default user data from DB')
108 user_id = user[0]
104
109
105 return user_id
110 return user_id
106
107
108 def get_default_base_path():
109 from sqlalchemy import text
110 from rhodecode.model import meta
111
112 engine = meta.get_engine()
113 with meta.SA_Session(engine) as session:
114 result = session.execute(text("SELECT ui_value from rhodecode_ui where ui_key = '/'"))
115 base_path = result.first()[0]
116
117 return base_path
@@ -15,13 +15,13 b''
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18 import logging
19 import logging
19 import datetime
20 import datetime
20 import typing
21
21
22 from zope.cachedescriptors.property import Lazy as LazyProperty
22 from zope.cachedescriptors.property import Lazy as LazyProperty
23 from pyramid.threadlocal import get_current_request
24
23
24 from rhodecode.lib.pyramid_utils import get_current_request
25 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.lib.utils2 import AttributeDict
26
26
27
27
@@ -41,8 +41,9 b' class RhodecodeEvent(object):'
41 name = "RhodeCodeEvent"
41 name = "RhodeCodeEvent"
42 no_url_set = '<no server_url available>'
42 no_url_set = '<no server_url available>'
43
43
44 def __init__(self, request=None):
44 def __init__(self, request=None, actor=None):
45 self._request = request
45 self._request = request
46 self._actor = actor
46 self.utc_timestamp = datetime.datetime.utcnow()
47 self.utc_timestamp = datetime.datetime.utcnow()
47
48
48 def __repr__(self):
49 def __repr__(self):
@@ -72,16 +73,24 b' class RhodecodeEvent(object):'
72
73
73 @property
74 @property
74 def actor(self):
75 def actor(self):
76 from rhodecode.lib.auth import AuthUser
77
78 # if an explicit actor is specified, use this
79 if self._actor:
80 return self._actor
81
75 auth_user = self.auth_user
82 auth_user = self.auth_user
76 if auth_user:
83 log.debug('Got integration actor: %s', auth_user)
84 if isinstance(auth_user, AuthUser):
77 instance = auth_user.get_instance()
85 instance = auth_user.get_instance()
86 # we can't find this DB user...
78 if not instance:
87 if not instance:
79 return AttributeDict(dict(
88 return AttributeDict(dict(
80 username=auth_user.username,
89 username=auth_user.username,
81 user_id=auth_user.user_id,
90 user_id=auth_user.user_id,
82 ))
91 ))
83 return instance
92 elif auth_user:
84
93 return auth_user
85 return SYSTEM_USER
94 return SYSTEM_USER
86
95
87 @property
96 @property
@@ -129,3 +138,4 b' class FtsBuild(RhodecodeEvent):'
129 """
138 """
130 name = 'fts-build'
139 name = 'fts-build'
131 display_name = 'Start FTS Build'
140 display_name = 'Start FTS Build'
141
@@ -156,11 +156,11 b' class RepoEvent(RhodeCodeIntegrationEven'
156 Base class for events acting on a repository.
156 Base class for events acting on a repository.
157 """
157 """
158
158
159 def __init__(self, repo):
159 def __init__(self, repo, actor=None):
160 """
160 """
161 :param repo: a :class:`Repository` instance
161 :param repo: a :class:`Repository` instance
162 """
162 """
163 super().__init__()
163 super().__init__(actor=actor)
164 self.repo = repo
164 self.repo = repo
165
165
166 def as_dict(self):
166 def as_dict(self):
@@ -11,9 +11,9 b' import importlib'
11 from inspect import istraceback
11 from inspect import istraceback
12
12
13 from collections import OrderedDict
13 from collections import OrderedDict
14 from rhodecode.lib.logging_formatter import _inject_req_id, ExceptionAwareFormatter
15 from rhodecode.lib.ext_json import sjson as json
16
14
15 from ...logging_formatter import _inject_req_id, ExceptionAwareFormatter
16 from ...ext_json import sjson as json
17
17
18 ZERO = timedelta(0)
18 ZERO = timedelta(0)
19 HOUR = timedelta(hours=1)
19 HOUR = timedelta(hours=1)
@@ -78,7 +78,7 b' class JsonEncoder(json.JSONEncoder):'
78 return str(obj)
78 return str(obj)
79
79
80 try:
80 try:
81 return super(JsonEncoder, self).default(obj)
81 return super().default(obj)
82
82
83 except TypeError:
83 except TypeError:
84 try:
84 try:
@@ -194,7 +194,7 b' class JsonFormatter(ExceptionAwareFormat'
194
194
195 def serialize_log_record(self, log_record):
195 def serialize_log_record(self, log_record):
196 """Returns the final representation of the log record."""
196 """Returns the final representation of the log record."""
197 return "%s%s" % (self.prefix, self.jsonify_log_record(log_record))
197 return "{}{}".format(self.prefix, self.jsonify_log_record(log_record))
198
198
199 def format(self, record):
199 def format(self, record):
200 """Formats a log record and serializes to json"""
200 """Formats a log record and serializes to json"""
@@ -102,7 +102,7 b' class NotExpirable(RuntimeError):'
102 pass
102 pass
103
103
104
104
105 class Lock(object):
105 class Lock:
106 """
106 """
107 A Lock context manager implemented via redis SETNX/BLPOP.
107 A Lock context manager implemented via redis SETNX/BLPOP.
108 """
108 """
@@ -111,11 +111,12 b' class Lock(object):'
111 extend_script = None
111 extend_script = None
112 reset_script = None
112 reset_script = None
113 reset_all_script = None
113 reset_all_script = None
114 blocking = None
114
115
115 _lock_renewal_interval: float
116 _lock_renewal_interval: float
116 _lock_renewal_thread: Union[threading.Thread, None]
117 _lock_renewal_thread: Union[threading.Thread, None]
117
118
118 def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False, strict=True, signal_expire=1000):
119 def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False, strict=True, signal_expire=1000, blocking=True):
119 """
120 """
120 :param redis_client:
121 :param redis_client:
121 An instance of :class:`~StrictRedis`.
122 An instance of :class:`~StrictRedis`.
@@ -143,6 +144,9 b' class Lock(object):'
143 If set ``True`` then the ``redis_client`` needs to be an instance of ``redis.StrictRedis``.
144 If set ``True`` then the ``redis_client`` needs to be an instance of ``redis.StrictRedis``.
144 :param signal_expire:
145 :param signal_expire:
145 Advanced option to override signal list expiration in milliseconds. Increase it for very slow clients. Default: ``1000``.
146 Advanced option to override signal list expiration in milliseconds. Increase it for very slow clients. Default: ``1000``.
147 :param blocking:
148 Boolean value specifying whether lock should be blocking or not.
149 Used in `__enter__` method.
146 """
150 """
147 if strict and not isinstance(redis_client, StrictRedis):
151 if strict and not isinstance(redis_client, StrictRedis):
148 raise ValueError("redis_client must be instance of StrictRedis. "
152 raise ValueError("redis_client must be instance of StrictRedis. "
@@ -179,6 +183,8 b' class Lock(object):'
179 else None)
183 else None)
180 self._lock_renewal_thread = None
184 self._lock_renewal_thread = None
181
185
186 self.blocking = blocking
187
182 self.register_scripts(redis_client)
188 self.register_scripts(redis_client)
183
189
184 @classmethod
190 @classmethod
@@ -342,9 +348,11 b' class Lock(object):'
342 loggers["refresh.exit"].debug("Renewal thread for Lock(%r) exited.", self._name)
348 loggers["refresh.exit"].debug("Renewal thread for Lock(%r) exited.", self._name)
343
349
344 def __enter__(self):
350 def __enter__(self):
345 acquired = self.acquire(blocking=True)
351 acquired = self.acquire(blocking=self.blocking)
346 if not acquired:
352 if not acquired:
353 if self.blocking:
347 raise AssertionError(f"Lock({self._name}) wasn't acquired, but blocking=True was used!")
354 raise AssertionError(f"Lock({self._name}) wasn't acquired, but blocking=True was used!")
355 raise NotAcquired(f"Lock({self._name}) is not acquired or it already expired.")
348 return self
356 return self
349
357
350 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
358 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
@@ -1,5 +1,3 b''
1
2
3 import logging
1 import logging
4
2
5 from .stream import TCPStatsClient, UnixSocketStatsClient # noqa
3 from .stream import TCPStatsClient, UnixSocketStatsClient # noqa
@@ -26,9 +24,10 b' def client_from_config(configuration, pr'
26 from pyramid.settings import asbool
24 from pyramid.settings import asbool
27
25
28 _config = statsd_config(configuration, prefix)
26 _config = statsd_config(configuration, prefix)
27 statsd_flag = _config.get('enabled')
29 statsd_enabled = asbool(_config.pop('enabled', False))
28 statsd_enabled = asbool(_config.pop('enabled', False))
30 if not statsd_enabled:
29 if not statsd_enabled:
31 log.debug('statsd client not enabled by statsd.enabled = flag, skipping...')
30 log.debug('statsd client not enabled by statsd.enabled = %s flag, skipping...', statsd_flag)
32 return
31 return
33
32
34 host = _config.pop('statsd_host', HOST)
33 host = _config.pop('statsd_host', HOST)
@@ -1,5 +1,3 b''
1
2
3 import re
1 import re
4 import random
2 import random
5 from collections import deque
3 from collections import deque
@@ -31,7 +29,7 b' def normalize_tags(tag_list):'
31 return _normalize_tags_with_cache(tuple(tag_list))
29 return _normalize_tags_with_cache(tuple(tag_list))
32
30
33
31
34 class StatsClientBase(object):
32 class StatsClientBase:
35 """A Base class for various statsd clients."""
33 """A Base class for various statsd clients."""
36
34
37 def close(self):
35 def close(self):
@@ -73,7 +71,7 b' class StatsClientBase(object):'
73
71
74 def incr(self, stat, count=1, rate=1, tags=None):
72 def incr(self, stat, count=1, rate=1, tags=None):
75 """Increment a stat by `count`."""
73 """Increment a stat by `count`."""
76 self._send_stat(stat, '%s|c' % count, rate, tags)
74 self._send_stat(stat, f'{count}|c', rate, tags)
77
75
78 def decr(self, stat, count=1, rate=1, tags=None):
76 def decr(self, stat, count=1, rate=1, tags=None):
79 """Decrement a stat by `count`."""
77 """Decrement a stat by `count`."""
@@ -87,18 +85,18 b' class StatsClientBase(object):'
87 return
85 return
88 with self.pipeline() as pipe:
86 with self.pipeline() as pipe:
89 pipe._send_stat(stat, '0|g', 1)
87 pipe._send_stat(stat, '0|g', 1)
90 pipe._send_stat(stat, '%s|g' % value, 1)
88 pipe._send_stat(stat, f'{value}|g', 1)
91 else:
89 else:
92 prefix = '+' if delta and value >= 0 else ''
90 prefix = '+' if delta and value >= 0 else ''
93 self._send_stat(stat, '%s%s|g' % (prefix, value), rate, tags)
91 self._send_stat(stat, f'{prefix}{value}|g', rate, tags)
94
92
95 def set(self, stat, value, rate=1):
93 def set(self, stat, value, rate=1):
96 """Set a set value."""
94 """Set a set value."""
97 self._send_stat(stat, '%s|s' % value, rate)
95 self._send_stat(stat, f'{value}|s', rate)
98
96
99 def histogram(self, stat, value, rate=1, tags=None):
97 def histogram(self, stat, value, rate=1, tags=None):
100 """Set a histogram"""
98 """Set a histogram"""
101 self._send_stat(stat, '%s|h' % value, rate, tags)
99 self._send_stat(stat, f'{value}|h', rate, tags)
102
100
103 def _send_stat(self, stat, value, rate, tags=None):
101 def _send_stat(self, stat, value, rate, tags=None):
104 self._after(self._prepare(stat, value, rate, tags))
102 self._after(self._prepare(stat, value, rate, tags))
@@ -110,10 +108,10 b' class StatsClientBase(object):'
110 if rate < 1:
108 if rate < 1:
111 if random.random() > rate:
109 if random.random() > rate:
112 return
110 return
113 value = '%s|@%s' % (value, rate)
111 value = f'{value}|@{rate}'
114
112
115 if self._prefix:
113 if self._prefix:
116 stat = '%s.%s' % (self._prefix, stat)
114 stat = f'{self._prefix}.{stat}'
117
115
118 res = '%s:%s%s' % (
116 res = '%s:%s%s' % (
119 stat,
117 stat,
@@ -1,5 +1,3 b''
1
2
3 import socket
1 import socket
4
2
5 from .base import StatsClientBase, PipelineBase
3 from .base import StatsClientBase, PipelineBase
@@ -1,5 +1,3 b''
1
2
3 import functools
1 import functools
4 from time import perf_counter as time_now
2 from time import perf_counter as time_now
5
3
@@ -11,7 +9,7 b' def safe_wraps(wrapper, *args, **kwargs)'
11 return functools.wraps(wrapper, *args, **kwargs)
9 return functools.wraps(wrapper, *args, **kwargs)
12
10
13
11
14 class Timer(object):
12 class Timer:
15 """A context manager/decorator for statsd.timing()."""
13 """A context manager/decorator for statsd.timing()."""
16
14
17 def __init__(self, client, stat, rate=1, tags=None, use_decimals=True, auto_send=True):
15 def __init__(self, client, stat, rate=1, tags=None, use_decimals=True, auto_send=True):
@@ -1,5 +1,3 b''
1
2
3 import socket
1 import socket
4
2
5 from .base import StatsClientBase, PipelineBase
3 from .base import StatsClientBase, PipelineBase
@@ -8,7 +6,7 b' from .base import StatsClientBase, Pipel'
8 class Pipeline(PipelineBase):
6 class Pipeline(PipelineBase):
9
7
10 def __init__(self, client):
8 def __init__(self, client):
11 super(Pipeline, self).__init__(client)
9 super().__init__(client)
12 self._maxudpsize = client._maxudpsize
10 self._maxudpsize = client._maxudpsize
13
11
14 def _send(self):
12 def _send(self):
@@ -258,8 +258,7 b' class ActionParser(object):'
258 commit = repo.get_commit(commit_id=commit_id)
258 commit = repo.get_commit(commit_id=commit_id)
259 commits.append(commit)
259 commits.append(commit)
260 except CommitDoesNotExistError:
260 except CommitDoesNotExistError:
261 log.error(
261 log.error('cannot find commit id %s in this repository',
262 'cannot find commit id %s in this repository',
263 commit_id)
262 commit_id)
264 commits.append(commit_id)
263 commits.append(commit_id)
265 continue
264 continue
@@ -1,4 +1,4 b''
1 # Copyright (C) 2015-2023 RhodeCode GmbH
1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
@@ -16,73 +16,162 b''
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import codecs
20 import hashlib
19 import logging
21 import logging
20 import os
22 import os
21 import diskcache
23 import typing
22 from diskcache import RLock
24
25 import fsspec
26
27 from .base import BaseCache, BaseShard
28 from ..utils import ShardFileReader, NOT_GIVEN
29 from ...type_utils import str2bool
23
30
24 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
25
32
26 cache_meta = None
33
34 class FileSystemShard(BaseShard):
35
36 def __init__(self, index, directory, directory_folder, fs, **settings):
37 self._index: int = index
38 self._directory: str = directory
39 self._directory_folder: str = directory_folder
40 self.storage_type: str = 'directory'
27
41
42 self.fs = fs
43
44 @property
45 def directory(self) -> str:
46 """Cache directory final path."""
47 return os.path.join(self._directory, self._directory_folder)
48
49 def _get_keyfile(self, archive_key) -> tuple[str, str]:
50 key_file: str = f'{archive_key}.{self.key_suffix}'
51 return key_file, os.path.join(self.directory, key_file)
28
52
29 class ReentrantLock(RLock):
53 def _get_writer(self, path, mode):
30 def __enter__(self):
54 for count in range(1, 11):
31 reentrant_lock_key = self._key
55 try:
56 # Another cache may have deleted the directory before
57 # the file could be opened.
58 return self.fs.open(path, mode)
59 except OSError:
60 if count == 10:
61 # Give up after 10 tries to open the file.
62 raise
63 continue
32
64
33 log.debug('Acquire ReentrantLock(key=%s) for archive cache generation...', reentrant_lock_key)
65 def _write_file(self, full_path, iterator, mode):
34 #self.acquire()
66
35 log.debug('Lock for key=%s acquired', reentrant_lock_key)
67 # ensure dir exists
68 destination, _ = os.path.split(full_path)
69 if not self.fs.exists(destination):
70 self.fs.makedirs(destination)
71
72 writer = self._get_writer(full_path, mode)
36
73
37 def __exit__(self, *exc_info):
74 digest = hashlib.sha256()
38 #self.release()
75 with writer:
39 pass
76 size = 0
77 for chunk in iterator:
78 size += len(chunk)
79 digest.update(chunk)
80 writer.write(chunk)
81 writer.flush()
82 # Get the file descriptor
83 fd = writer.fileno()
40
84
85 # Sync the file descriptor to disk, helps with NFS cases...
86 os.fsync(fd)
87 sha256 = digest.hexdigest()
88 log.debug('written new archive cache under %s, sha256: %s', full_path, sha256)
89 return size, sha256
41
90
42 def get_archival_config(config):
91 def store(self, key, value_reader, metadata: dict | None = None):
92 return self._store(key, value_reader, metadata, mode='xb')
43
93
44 final_config = {
94 def fetch(self, key, retry=NOT_GIVEN,
45 'archive_cache.eviction_policy': 'least-frequently-used'
95 retry_attempts=NOT_GIVEN, retry_backoff=1, **kwargs) -> tuple[ShardFileReader, dict]:
46 }
96 return self._fetch(key, retry, retry_attempts, retry_backoff)
97
98 def remove(self, key):
99 return self._remove(key)
100
101 def random_filename(self):
102 """Return filename and full-path tuple for file storage.
47
103
48 for k, v in config.items():
104 Filename will be a randomly generated 28 character hexadecimal string
49 if k.startswith('archive_cache'):
105 with ".archive_cache" suffixed. Two levels of sub-directories will be used to
50 final_config[k] = v
106 reduce the size of directories. On older filesystems, lookups in
107 directories with many files may be slow.
108 """
109
110 hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8')
51
111
52 return final_config
112 archive_name = hex_name[4:] + '.archive_cache'
113 filename = f"{hex_name[:2]}/{hex_name[2:4]}/{archive_name}"
114
115 full_path = os.path.join(self.directory, filename)
116 return archive_name, full_path
117
118 def __repr__(self):
119 return f'{self.__class__.__name__}(index={self._index}, dir={self.directory})'
53
120
54
121
55 def get_archival_cache_store(config):
122 class FileSystemFanoutCache(BaseCache):
123 shard_name: str = 'shard_{:03d}'
124 shard_cls = FileSystemShard
56
125
57 global cache_meta
126 def __init__(self, locking_url, **settings):
58 if cache_meta is not None:
127 """
59 return cache_meta
128 Initialize file system cache instance.
129
130 :param str locking_url: redis url for a lock
131 :param settings: settings dict
60
132
61 config = get_archival_config(config)
133 """
134 self._locking_url = locking_url
135 self._config = settings
136 cache_dir = self.get_conf('archive_cache.filesystem.store_dir')
137 directory = str(cache_dir)
138 directory = os.path.expanduser(directory)
139 directory = os.path.expandvars(directory)
140 self._directory = directory
141 self._storage_path = directory # common path for all from BaseCache
62
142
63 archive_cache_dir = config['archive_cache.store_dir']
143 self._shard_count = int(self.get_conf('archive_cache.filesystem.cache_shards', pop=True))
64 archive_cache_size_gb = config['archive_cache.cache_size_gb']
144 if self._shard_count < 1:
65 archive_cache_shards = config['archive_cache.cache_shards']
145 raise ValueError('cache_shards must be 1 or more')
66 archive_cache_eviction_policy = config['archive_cache.eviction_policy']
67
146
68 log.debug('Initializing archival cache instance under %s', archive_cache_dir)
147 self._eviction_policy = self.get_conf('archive_cache.filesystem.eviction_policy', pop=True)
148 self._cache_size_limit = self.gb_to_bytes(int(self.get_conf('archive_cache.filesystem.cache_size_gb')))
69
149
70 # check if it's ok to write, and re-create the archive cache
150 self.retry = str2bool(self.get_conf('archive_cache.filesystem.retry', pop=True))
71 if not os.path.isdir(archive_cache_dir):
151 self.retry_attempts = int(self.get_conf('archive_cache.filesystem.retry_attempts', pop=True))
72 os.makedirs(archive_cache_dir, exist_ok=True)
152 self.retry_backoff = int(self.get_conf('archive_cache.filesystem.retry_backoff', pop=True))
153
154 log.debug('Initializing %s archival cache instance', self)
155 fs = fsspec.filesystem('file')
156 # check if it's ok to write, and re-create the archive cache main dir
157 # A directory is the virtual equivalent of a physical file cabinet.
158 # In other words, it's a container for organizing digital data.
159 # Unlike a folder, which can only store files, a directory can store files,
160 # subdirectories, and other directories.
161 if not fs.exists(self._directory):
162 fs.makedirs(self._directory, exist_ok=True)
73
163
74 d_cache = diskcache.FanoutCache(
164 self._shards = tuple(
75 archive_cache_dir, shards=archive_cache_shards,
165 self.shard_cls(
76 cull_limit=0, # manual eviction required
166 index=num,
77 size_limit=archive_cache_size_gb * 1024 * 1024 * 1024,
167 directory=directory,
78 eviction_policy=archive_cache_eviction_policy,
168 directory_folder=self.shard_name.format(num),
79 timeout=30
169 fs=fs,
170 **settings,
80 )
171 )
81 cache_meta = d_cache
172 for num in range(self._shard_count)
82 return cache_meta
173 )
83
174 self._hash = self._shards[0].hash
84
175
85 def includeme(config):
176 def _get_size(self, shard, archive_path):
86 # init our cache at start
177 return os.stat(archive_path).st_size
87 settings = config.get_settings()
88 get_archival_cache_store(settings)
@@ -1688,7 +1688,7 b' def get_csrf_token(session, force_new=Fa'
1688
1688
1689
1689
1690 def get_request(perm_class_instance):
1690 def get_request(perm_class_instance):
1691 from pyramid.threadlocal import get_current_request
1691 from rhodecode.lib.pyramid_utils import get_current_request
1692 pyramid_request = get_current_request()
1692 pyramid_request = get_current_request()
1693 return pyramid_request
1693 return pyramid_request
1694
1694
@@ -347,8 +347,6 b' def attach_context_attributes(context, r'
347 context.ssh_key_generator_enabled = str2bool(
347 context.ssh_key_generator_enabled = str2bool(
348 config.get('ssh.enable_ui_key_generator', 'true'))
348 config.get('ssh.enable_ui_key_generator', 'true'))
349
349
350 context.visual.allow_repo_location_change = str2bool(
351 config.get('allow_repo_location_change', True))
352 context.visual.allow_custom_hooks_settings = str2bool(
350 context.visual.allow_custom_hooks_settings = str2bool(
353 config.get('allow_custom_hooks_settings', True))
351 config.get('allow_custom_hooks_settings', True))
354 context.debug_style = str2bool(config.get('debug_style', False))
352 context.debug_style = str2bool(config.get('debug_style', False))
@@ -567,7 +565,7 b' def add_events_routes(config):'
567
565
568
566
569 def bootstrap_config(request, registry_name='RcTestRegistry'):
567 def bootstrap_config(request, registry_name='RcTestRegistry'):
570 from rhodecode.config.middleware import sanitize_settings_and_apply_defaults
568 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
571 import pyramid.testing
569 import pyramid.testing
572 registry = pyramid.testing.Registry(registry_name)
570 registry = pyramid.testing.Registry(registry_name)
573
571
@@ -580,7 +578,7 b' def bootstrap_config(request, registry_n'
580 config.include('pyramid_mako')
578 config.include('pyramid_mako')
581 config.include('rhodecode.lib.rc_beaker')
579 config.include('rhodecode.lib.rc_beaker')
582 config.include('rhodecode.lib.rc_cache')
580 config.include('rhodecode.lib.rc_cache')
583 config.include('rhodecode.lib.rc_cache.archive_cache')
581 config.include('rhodecode.lib.archive_cache')
584 add_events_routes(config)
582 add_events_routes(config)
585
583
586 return config
584 return config
@@ -193,6 +193,7 b' def create_repo(form_data, cur_user):'
193 enable_downloads=enable_downloads,
193 enable_downloads=enable_downloads,
194 state=state
194 state=state
195 )
195 )
196
196 Session().commit()
197 Session().commit()
197
198
198 # now create this repo on Filesystem
199 # now create this repo on Filesystem
@@ -402,6 +403,11 b' def sync_last_update_for_objects(*args, '
402
403
403
404
404 @async_task(ignore_result=True, base=RequestContextTask)
405 @async_task(ignore_result=True, base=RequestContextTask)
406 def test_celery_exception(msg):
407 raise Exception(f'Test exception: {msg}')
408
409
410 @async_task(ignore_result=True, base=RequestContextTask)
405 def sync_last_update(*args, **kwargs):
411 def sync_last_update(*args, **kwargs):
406 sync_last_update_for_objects(*args, **kwargs)
412 sync_last_update_for_objects(*args, **kwargs)
407
413
@@ -201,7 +201,7 b' class DbManage(object):'
201 f'version {curr_version} to version {__dbversion__}')
201 f'version {curr_version} to version {__dbversion__}')
202
202
203 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
203 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
204 _step = None
204 final_step = 'latest'
205 for step in upgrade_steps:
205 for step in upgrade_steps:
206 notify(f'performing upgrade step {step}')
206 notify(f'performing upgrade step {step}')
207 time.sleep(0.5)
207 time.sleep(0.5)
@@ -210,10 +210,10 b' class DbManage(object):'
210 self.sa.rollback()
210 self.sa.rollback()
211 notify(f'schema upgrade for step {step} completed')
211 notify(f'schema upgrade for step {step} completed')
212
212
213 _step = step
213 final_step = step
214
214
215 self.run_post_migration_tasks()
215 self.run_post_migration_tasks()
216 notify(f'upgrade to version {step} successful')
216 notify(f'upgrade to version {final_step} successful')
217
217
218 def fix_repo_paths(self):
218 def fix_repo_paths(self):
219 """
219 """
@@ -1,3 +1,21 b''
1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
1 from rhodecode.lib.str_utils import safe_bytes
19 from rhodecode.lib.str_utils import safe_bytes
2 from rhodecode.lib.encrypt import encrypt_data, validate_and_decrypt_data
20 from rhodecode.lib.encrypt import encrypt_data, validate_and_decrypt_data
3 from rhodecode.lib.encrypt2 import Encryptor
21 from rhodecode.lib.encrypt2 import Encryptor
@@ -9,6 +27,10 b' def get_default_algo():'
9 import rhodecode
27 import rhodecode
10 return rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
28 return rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
11
29
30 def get_strict_mode():
31 import rhodecode
32 return rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict') or False
33
12
34
13 def encrypt_value(value: bytes, enc_key: bytes, algo: str = ''):
35 def encrypt_value(value: bytes, enc_key: bytes, algo: str = ''):
14 if not algo:
36 if not algo:
@@ -29,16 +51,21 b' def encrypt_value(value: bytes, enc_key:'
29 return value
51 return value
30
52
31
53
32 def decrypt_value(value: bytes, enc_key: bytes, algo: str = '', strict_mode: bool = False):
54 def decrypt_value(value: bytes, enc_key: bytes, algo: str = '', strict_mode: bool | None = None):
55
56 if strict_mode is None:
57 # we use config value rather then explicit True/False
58 strict_mode = get_strict_mode()
59
60 enc_key = safe_bytes(enc_key)
61 value = safe_bytes(value)
33
62
34 if not algo:
63 if not algo:
35 # not explicit algo, just use what's set by config
64 # not explicit algo, just use what's set by config
36 algo = get_default_algo()
65 algo = Encryptor.detect_enc_algo(value) or get_default_algo()
37 if algo not in ALLOWED_ALGOS:
66 if algo not in ALLOWED_ALGOS:
38 ValueError(f'Bad encryption algorithm, should be {ALLOWED_ALGOS}, got: {algo}')
67 ValueError(f'Bad encryption algorithm, should be {ALLOWED_ALGOS}, got: {algo}')
39
68
40 enc_key = safe_bytes(enc_key)
41 value = safe_bytes(value)
42 safe = not strict_mode
69 safe = not strict_mode
43
70
44 if algo == 'aes':
71 if algo == 'aes':
@@ -43,6 +43,7 b' class InvalidDecryptedValue(str):'
43 content = f'<{cls.__name__}({content[:16]}...)>'
43 content = f'<{cls.__name__}({content[:16]}...)>'
44 return str.__new__(cls, content)
44 return str.__new__(cls, content)
45
45
46
46 KEY_FORMAT = b'enc$aes_hmac${1}'
47 KEY_FORMAT = b'enc$aes_hmac${1}'
47
48
48
49
@@ -23,8 +23,25 b' class InvalidDecryptedValue(str):'
23
23
24 class Encryptor(object):
24 class Encryptor(object):
25 key_format = b'enc2$salt:{1}$data:{2}'
25 key_format = b'enc2$salt:{1}$data:{2}'
26
26 pref_len = 5 # salt:, data:
27 pref_len = 5 # salt:, data:
27
28
29 @classmethod
30 def detect_enc_algo(cls, enc_data: bytes):
31 parts = enc_data.split(b'$', 3)
32
33 if b'enc$aes_hmac$' in enc_data:
34 # we expect this data is encrypted, so validate the header
35 if len(parts) != 3:
36 raise ValueError(f'Encrypted Data has invalid format, expected {cls.key_format}, got `{parts}`')
37 return 'aes'
38 elif b'enc2$salt' in enc_data:
39 # we expect this data is encrypted, so validate the header
40 if len(parts) != 3:
41 raise ValueError(f'Encrypted Data has invalid format, expected {cls.key_format}, got `{parts}`')
42 return 'fernet'
43 return None
44
28 def __init__(self, enc_key: bytes):
45 def __init__(self, enc_key: bytes):
29 self.enc_key = enc_key
46 self.enc_key = enc_key
30
47
@@ -52,7 +69,7 b' class Encryptor(object):'
52 def _get_parts(self, enc_data):
69 def _get_parts(self, enc_data):
53 parts = enc_data.split(b'$', 3)
70 parts = enc_data.split(b'$', 3)
54 if len(parts) != 3:
71 if len(parts) != 3:
55 raise ValueError(f'Encrypted Data has invalid format, expected {self.key_format}, got {parts}')
72 raise ValueError(f'Encrypted Data has invalid format, expected {self.key_format}, got `{parts}`')
56 prefix, salt, enc_data = parts
73 prefix, salt, enc_data = parts
57
74
58 try:
75 try:
@@ -144,6 +144,10 b' class NotAllowedToCreateUserError(Except'
144 pass
144 pass
145
145
146
146
147 class DuplicateUpdateUserError(Exception):
148 pass
149
150
147 class RepositoryCreationError(Exception):
151 class RepositoryCreationError(Exception):
148 pass
152 pass
149
153
@@ -74,6 +74,7 b' from webhelpers2.html.tags import ('
74
74
75 from webhelpers2.number import format_byte_size
75 from webhelpers2.number import format_byte_size
76 # python3.11 backport fixes for webhelpers2
76 # python3.11 backport fixes for webhelpers2
77 from rhodecode import ConfigGet
77 from rhodecode.lib._vendor.webhelpers_backports import raw_select
78 from rhodecode.lib._vendor.webhelpers_backports import raw_select
78
79
79 from rhodecode.lib.action_parser import action_parser
80 from rhodecode.lib.action_parser import action_parser
@@ -916,9 +917,7 b' def get_repo_type_by_name(repo_name):'
916
917
917 def is_svn_without_proxy(repository):
918 def is_svn_without_proxy(repository):
918 if is_svn(repository):
919 if is_svn(repository):
919 from rhodecode.model.settings import VcsSettingsModel
920 return not ConfigGet().get_bool('vcs.svn.proxy.enabled')
920 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
921 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
922 return False
921 return False
923
922
924
923
@@ -2197,3 +2196,35 b' class IssuesRegistry(object):'
2197 @property
2196 @property
2198 def issues_unique_count(self):
2197 def issues_unique_count(self):
2199 return len(set(i['id'] for i in self.issues))
2198 return len(set(i['id'] for i in self.issues))
2199
2200
2201 def get_directory_statistics(start_path):
2202 """
2203 total_files, total_size, directory_stats = get_directory_statistics(start_path)
2204
2205 print(f"Directory statistics for: {start_path}\n")
2206 print(f"Total files: {total_files}")
2207 print(f"Total size: {format_size(total_size)}\n")
2208
2209 :param start_path:
2210 :return:
2211 """
2212
2213 total_files = 0
2214 total_size = 0
2215 directory_stats = {}
2216
2217 for dir_path, dir_names, file_names in os.walk(start_path):
2218 dir_size = 0
2219 file_count = len(file_names)
2220
2221 for fname in file_names:
2222 filepath = os.path.join(dir_path, fname)
2223 file_size = os.path.getsize(filepath)
2224 dir_size += file_size
2225
2226 directory_stats[dir_path] = {'file_count': file_count, 'size': dir_size}
2227 total_files += file_count
2228 total_size += dir_size
2229
2230 return total_files, total_size, directory_stats
@@ -17,7 +17,6 b''
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import webob
19 import webob
20 from pyramid.threadlocal import get_current_request
21
20
22 from rhodecode import events
21 from rhodecode import events
23 from rhodecode.lib import hooks_base
22 from rhodecode.lib import hooks_base
@@ -33,6 +32,7 b' def _supports_repo_type(repo_type):'
33 def _get_vcs_operation_context(username, repo_name, repo_type, action):
32 def _get_vcs_operation_context(username, repo_name, repo_type, action):
34 # NOTE(dan): import loop
33 # NOTE(dan): import loop
35 from rhodecode.lib.base import vcs_operation_context
34 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.pyramid_utils import get_current_request
36
36
37 check_locking = action in ('pull', 'push')
37 check_locking = action in ('pull', 'push')
38
38
@@ -141,7 +141,7 b' class ColorFormatter(ExceptionAwareForma'
141 """
141 """
142 Changes record's levelname to use with COLORS enum
142 Changes record's levelname to use with COLORS enum
143 """
143 """
144 def_record = super(ColorFormatter, self).format(record)
144 def_record = super().format(record)
145
145
146 levelname = record.levelname
146 levelname = record.levelname
147 start = COLOR_SEQ % (COLORS[levelname])
147 start = COLOR_SEQ % (COLORS[levelname])
@@ -17,7 +17,8 b''
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import base64
20 import re
21 import os
21 import logging
22 import logging
22 import urllib.request
23 import urllib.request
23 import urllib.parse
24 import urllib.parse
@@ -27,15 +28,11 b' import urllib.parse'
27 import requests
28 import requests
28 from pyramid.httpexceptions import HTTPNotAcceptable
29 from pyramid.httpexceptions import HTTPNotAcceptable
29
30
30 from rhodecode.lib import rc_cache
31 from rhodecode import ConfigGet
31 from rhodecode.lib.middleware import simplevcs
32 from rhodecode.lib.middleware import simplevcs
32 from rhodecode.lib.middleware.utils import get_path_info
33 from rhodecode.lib.middleware.utils import get_path_info
33 from rhodecode.lib.utils import is_valid_repo
34 from rhodecode.lib.utils import is_valid_repo
34 from rhodecode.lib.str_utils import safe_str, safe_int, safe_bytes
35 from rhodecode.lib.str_utils import safe_str
35 from rhodecode.lib.type_utils import str2bool
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.hooks_daemon import store_txn_id_data
38
39
36
40 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
41
38
@@ -54,37 +51,20 b' class SimpleSvnApp(object):'
54 request_headers = self._get_request_headers(environ)
51 request_headers = self._get_request_headers(environ)
55 data_io = environ['wsgi.input']
52 data_io = environ['wsgi.input']
56 req_method: str = environ['REQUEST_METHOD']
53 req_method: str = environ['REQUEST_METHOD']
57 has_content_length = 'CONTENT_LENGTH' in environ
54 has_content_length: bool = 'CONTENT_LENGTH' in environ
58
55
59 path_info = self._get_url(
56 path_info = self._get_url(
60 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
57 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
61 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
58 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
62 log.debug('Handling: %s method via `%s`', req_method, path_info)
59 log.debug('Handling: %s method via `%s` has_content_length:%s', req_method, path_info, has_content_length)
63
60
64 # stream control flag, based on request and content type...
61 # stream control flag, based on request and content type...
65 stream = False
62 stream = False
66
67 if req_method in ['MKCOL'] or has_content_length:
63 if req_method in ['MKCOL'] or has_content_length:
68 data_processed = False
69 # read chunk to check if we have txn-with-props
70 initial_data: bytes = data_io.read(1024)
71 if initial_data.startswith(b'(create-txn-with-props'):
72 data_io = initial_data + data_io.read()
73 # store on-the-fly our rc_extra using svn revision properties
74 # those can be read later on in hooks executed so we have a way
75 # to pass in the data into svn hooks
76 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
77 rc_data_len = str(len(rc_data))
78 # header defines data length, and serialized data
79 skel = b' rc-scm-extras %b %b' % (safe_bytes(rc_data_len), safe_bytes(rc_data))
80 data_io = data_io[:-2] + skel + b'))'
81 data_processed = True
82
83 if not data_processed:
84 # NOTE(johbo): Avoid that we end up with sending the request in chunked
64 # NOTE(johbo): Avoid that we end up with sending the request in chunked
85 # transfer encoding (mainly on Gunicorn). If we know the content
65 # transfer encoding (mainly on Gunicorn). If we know the content
86 # length, then we should transfer the payload in one request.
66 # length, then we should transfer the payload in one request.
87 data_io = initial_data + data_io.read()
67 data_io = data_io.read()
88
68
89 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
69 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
90 # NOTE(marcink): when getting/uploading files, we want to STREAM content
70 # NOTE(marcink): when getting/uploading files, we want to STREAM content
@@ -101,6 +81,7 b' class SimpleSvnApp(object):'
101 stream=stream
81 stream=stream
102 )
82 )
103 if req_method in ['HEAD', 'DELETE']:
83 if req_method in ['HEAD', 'DELETE']:
84 # NOTE(marcink): HEAD might be deprecated for SVN 1.14+ protocol
104 del call_kwargs['data']
85 del call_kwargs['data']
105
86
106 try:
87 try:
@@ -120,14 +101,6 b' class SimpleSvnApp(object):'
120 log.debug('got response code: %s', response.status_code)
101 log.debug('got response code: %s', response.status_code)
121
102
122 response_headers = self._get_response_headers(response.headers)
103 response_headers = self._get_response_headers(response.headers)
123
124 if response.headers.get('SVN-Txn-name'):
125 svn_tx_id = response.headers.get('SVN-Txn-name')
126 txn_id = rc_cache.utils.compute_key_from_params(
127 self.config['repository'], svn_tx_id)
128 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
129 store_txn_id_data(txn_id, {'port': port})
130
131 start_response(f'{response.status_code} {response.reason}', response_headers)
104 start_response(f'{response.status_code} {response.reason}', response_headers)
132 return response.iter_content(chunk_size=1024)
105 return response.iter_content(chunk_size=1024)
133
106
@@ -137,6 +110,20 b' class SimpleSvnApp(object):'
137 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
110 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
138 return url_path
111 return url_path
139
112
113 def _get_txn_id(self, environ):
114 url = environ['RAW_URI']
115
116 # Define the regex pattern
117 pattern = r'/txr/([^/]+)/'
118
119 # Search for the pattern in the URL
120 match = re.search(pattern, url)
121
122 # Check if a match is found and extract the captured group
123 if match:
124 txn_id = match.group(1)
125 return txn_id
126
140 def _get_request_headers(self, environ):
127 def _get_request_headers(self, environ):
141 headers = {}
128 headers = {}
142 whitelist = {
129 whitelist = {
@@ -182,10 +169,39 b' class DisabledSimpleSvnApp(object):'
182
169
183
170
184 class SimpleSvn(simplevcs.SimpleVCS):
171 class SimpleSvn(simplevcs.SimpleVCS):
172 """
173 details: https://svn.apache.org/repos/asf/subversion/trunk/notes/http-and-webdav/webdav-protocol
174
175 Read Commands : (OPTIONS, PROPFIND, GET, REPORT)
176
177 GET: fetch info about resources
178 PROPFIND: Used to retrieve properties of resources.
179 REPORT: Used for specialized queries to the repository. E.g History etc...
180 OPTIONS: request is sent to an SVN server, the server responds with information about the available HTTP
181 methods and other server capabilities.
182
183 Write Commands : (MKACTIVITY, PROPPATCH, PUT, CHECKOUT, MKCOL, MOVE,
184 -------------- COPY, DELETE, LOCK, UNLOCK, MERGE)
185
186 With the exception of LOCK/UNLOCK, every write command performs some
187 sort of DeltaV commit operation. In DeltaV, a commit always starts
188 by creating a transaction (MKACTIVITY), applies a log message
189 (PROPPATCH), does some other write methods, and then ends by
190 committing the transaction (MERGE). If the MERGE fails, the client
191 may try to remove the transaction with a DELETE.
192
193 PROPPATCH: Used to set and/or remove properties on resources.
194 MKCOL: Creates a new collection (directory).
195 DELETE: Removes a resource.
196 COPY and MOVE: Used for copying and moving resources.
197 MERGE: Used to merge changes from different branches.
198 CHECKOUT, CHECKIN, UNCHECKOUT: DeltaV methods for managing working resources and versions.
199 """
185
200
186 SCM = 'svn'
201 SCM = 'svn'
187 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
202 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
188 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
203 WRITE_COMMANDS = ('MERGE', 'POST', 'PUT', 'COPY', 'MOVE', 'DELETE', 'MKCOL')
204 DEFAULT_HTTP_SERVER = 'http://svn:8090'
189
205
190 def _get_repository_name(self, environ):
206 def _get_repository_name(self, environ):
191 """
207 """
@@ -218,10 +234,10 b' class SimpleSvn(simplevcs.SimpleVCS):'
218 else 'push')
234 else 'push')
219
235
220 def _should_use_callback_daemon(self, extras, environ, action):
236 def _should_use_callback_daemon(self, extras, environ, action):
221 # only MERGE command triggers hooks, so we don't want to start
237 # only PUT & MERGE command triggers hooks, so we don't want to start
222 # hooks server too many times. POST however starts the svn transaction
238 # hooks server too many times. POST however starts the svn transaction
223 # so we also need to run the init of callback daemon of POST
239 # so we also need to run the init of callback daemon of POST
224 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
240 if environ['REQUEST_METHOD'] not in self.READ_ONLY_COMMANDS:
225 return True
241 return True
226 return False
242 return False
227
243
@@ -232,12 +248,10 b' class SimpleSvn(simplevcs.SimpleVCS):'
232 return DisabledSimpleSvnApp(config)
248 return DisabledSimpleSvnApp(config)
233
249
234 def _is_svn_enabled(self):
250 def _is_svn_enabled(self):
235 conf = self.repo_vcs_config
251 return ConfigGet().get_bool('vcs.svn.proxy.enabled')
236 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
237
252
238 def _create_config(self, extras, repo_name, scheme='http'):
253 def _create_config(self, extras, repo_name, scheme='http'):
239 conf = self.repo_vcs_config
254 server_url = ConfigGet().get_str('vcs.svn.proxy.host')
240 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
241 server_url = server_url or self.DEFAULT_HTTP_SERVER
255 server_url = server_url or self.DEFAULT_HTTP_SERVER
242
256
243 extras['subversion_http_server_url'] = server_url
257 extras['subversion_http_server_url'] = server_url
@@ -25,11 +25,9 b" It's implemented with basic auth functio"
25
25
26 import os
26 import os
27 import re
27 import re
28 import io
29 import logging
28 import logging
30 import importlib
29 import importlib
31 from functools import wraps
30 from functools import wraps
32 from lxml import etree
33
31
34 import time
32 import time
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
@@ -41,14 +39,15 b' from zope.cachedescriptors.property impo'
41 import rhodecode
39 import rhodecode
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
40 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 from rhodecode.lib import rc_cache
41 from rhodecode.lib import rc_cache
42 from rhodecode.lib.svn_txn_utils import store_txn_id_data
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 from rhodecode.lib.base import (
44 from rhodecode.lib.base import (
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
46 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
47 from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
49 from rhodecode.lib.middleware import appenlight
48 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware.utils import scm_app_http
49 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.str_utils import safe_bytes
50 from rhodecode.lib.str_utils import safe_bytes, safe_int
52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
@@ -63,29 +62,6 b' from rhodecode.model.settings import Set'
63 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
64
63
65
64
66 def extract_svn_txn_id(acl_repo_name, data: bytes):
67 """
68 Helper method for extraction of svn txn_id from submitted XML data during
69 POST operations
70 """
71
72 try:
73 root = etree.fromstring(data)
74 pat = re.compile(r'/txn/(?P<txn_id>.*)')
75 for el in root:
76 if el.tag == '{DAV:}source':
77 for sub_el in el:
78 if sub_el.tag == '{DAV:}href':
79 match = pat.search(sub_el.text)
80 if match:
81 svn_tx_id = match.groupdict()['txn_id']
82 txn_id = rc_cache.utils.compute_key_from_params(
83 acl_repo_name, svn_tx_id)
84 return txn_id
85 except Exception:
86 log.exception('Failed to extract txn_id')
87
88
89 def initialize_generator(factory):
65 def initialize_generator(factory):
90 """
66 """
91 Initializes the returned generator by draining its first element.
67 Initializes the returned generator by draining its first element.
@@ -156,17 +132,10 b' class SimpleVCS(object):'
156
132
157 @property
133 @property
158 def base_path(self):
134 def base_path(self):
159 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
135 settings_path = self.config.get('repo_store.path')
160
161 if not settings_path:
162 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
163
136
164 if not settings_path:
137 if not settings_path:
165 # try, maybe we passed in explicitly as config option
138 raise ValueError('FATAL: repo_store.path is empty')
166 settings_path = self.config.get('base_path')
167
168 if not settings_path:
169 raise ValueError('FATAL: base_path is empty')
170 return settings_path
139 return settings_path
171
140
172 def set_repo_names(self, environ):
141 def set_repo_names(self, environ):
@@ -475,7 +444,6 b' class SimpleVCS(object):'
475 log.debug('Not enough credentials to access repo: `%s` '
444 log.debug('Not enough credentials to access repo: `%s` '
476 'repository as anonymous user', self.acl_repo_name)
445 'repository as anonymous user', self.acl_repo_name)
477
446
478
479 username = None
447 username = None
480 # ==============================================================
448 # ==============================================================
481 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
449 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
@@ -589,6 +557,24 b' class SimpleVCS(object):'
589 return self._generate_vcs_response(
557 return self._generate_vcs_response(
590 environ, start_response, repo_path, extras, action)
558 environ, start_response, repo_path, extras, action)
591
559
560 def _get_txn_id(self, environ):
561
562 for k in ['RAW_URI', 'HTTP_DESTINATION']:
563 url = environ.get(k)
564 if not url:
565 continue
566
567 # regex to search for svn-txn-id
568 pattern = r'/!svn/txr/([^/]+)/'
569
570 # Search for the pattern in the URL
571 match = re.search(pattern, url)
572
573 # Check if a match is found and extract the captured group
574 if match:
575 txn_id = match.group(1)
576 return txn_id
577
592 @initialize_generator
578 @initialize_generator
593 def _generate_vcs_response(
579 def _generate_vcs_response(
594 self, environ, start_response, repo_path, extras, action):
580 self, environ, start_response, repo_path, extras, action):
@@ -600,28 +586,23 b' class SimpleVCS(object):'
600 also handles the locking exceptions which will be triggered when
586 also handles the locking exceptions which will be triggered when
601 the first chunk is produced by the underlying WSGI application.
587 the first chunk is produced by the underlying WSGI application.
602 """
588 """
603
589 svn_txn_id = ''
604 txn_id = ''
590 if action == 'push':
605 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
591 svn_txn_id = self._get_txn_id(environ)
606 # case for SVN, we want to re-use the callback daemon port
607 # so we use the txn_id, for this we peek the body, and still save
608 # it as wsgi.input
609
610 stream = environ['wsgi.input']
611
612 if isinstance(stream, io.BytesIO):
613 data: bytes = stream.getvalue()
614 elif hasattr(stream, 'buf'): # most likely gunicorn.http.body.Body
615 data: bytes = stream.buf.getvalue()
616 else:
617 # fallback to the crudest way, copy the iterator
618 data = safe_bytes(stream.read())
619 environ['wsgi.input'] = io.BytesIO(data)
620
621 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
622
592
623 callback_daemon, extras = self._prepare_callback_daemon(
593 callback_daemon, extras = self._prepare_callback_daemon(
624 extras, environ, action, txn_id=txn_id)
594 extras, environ, action, txn_id=svn_txn_id)
595
596 if svn_txn_id:
597
598 port = safe_int(extras['hooks_uri'].split(':')[-1])
599 txn_id_data = extras.copy()
600 txn_id_data.update({'port': port})
601 txn_id_data.update({'req_method': environ['REQUEST_METHOD']})
602
603 full_repo_path = repo_path
604 store_txn_id_data(full_repo_path, svn_txn_id, txn_id_data)
605
625 log.debug('HOOKS extras is %s', extras)
606 log.debug('HOOKS extras is %s', extras)
626
607
627 http_scheme = self._get_http_scheme(environ)
608 http_scheme = self._get_http_scheme(environ)
@@ -684,6 +665,7 b' class SimpleVCS(object):'
684
665
685 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
666 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
686 protocol = vcs_settings.HOOKS_PROTOCOL
667 protocol = vcs_settings.HOOKS_PROTOCOL
668
687 if not self._should_use_callback_daemon(extras, environ, action):
669 if not self._should_use_callback_daemon(extras, environ, action):
688 # disable callback daemon for actions that don't require it
670 # disable callback daemon for actions that don't require it
689 protocol = 'local'
671 protocol = 'local'
@@ -26,6 +26,7 b' import urllib.parse'
26 from webob.exc import HTTPNotFound
26 from webob.exc import HTTPNotFound
27
27
28 import rhodecode
28 import rhodecode
29 from rhodecode.apps._base import ADMIN_PREFIX
29 from rhodecode.lib.middleware.utils import get_path_info
30 from rhodecode.lib.middleware.utils import get_path_info
30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
31 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
32 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
@@ -164,14 +165,18 b' def detect_vcs_request(environ, backends'
164 # login
165 # login
165 "_admin/login",
166 "_admin/login",
166
167
168 # 2fa
169 f"{ADMIN_PREFIX}/check_2fa",
170 f"{ADMIN_PREFIX}/setup_2fa",
171
167 # _admin/api is safe too
172 # _admin/api is safe too
168 '_admin/api',
173 f'{ADMIN_PREFIX}/api',
169
174
170 # _admin/gist is safe too
175 # _admin/gist is safe too
171 '_admin/gists++',
176 f'{ADMIN_PREFIX}/gists++',
172
177
173 # _admin/my_account is safe too
178 # _admin/my_account is safe too
174 '_admin/my_account++',
179 f'{ADMIN_PREFIX}/my_account++',
175
180
176 # static files no detection
181 # static files no detection
177 '_static++',
182 '_static++',
@@ -180,11 +185,11 b' def detect_vcs_request(environ, backends'
180 '_debug_toolbar++',
185 '_debug_toolbar++',
181
186
182 # skip ops ping, status
187 # skip ops ping, status
183 '_admin/ops/ping',
188 f'{ADMIN_PREFIX}/ops/ping',
184 '_admin/ops/status',
189 f'{ADMIN_PREFIX}/ops/status',
185
190
186 # full channelstream connect should be VCS skipped
191 # full channelstream connect should be VCS skipped
187 '_admin/channelstream/connect',
192 f'{ADMIN_PREFIX}/channelstream/connect',
188
193
189 '++/repo_creating_check'
194 '++/repo_creating_check'
190 ]
195 ]
@@ -41,6 +41,12 b' or reset some user/system settings.'
41 """
41 """
42
42
43
43
44 def import_all_from_module(module_name):
45 import importlib
46 module = importlib.import_module(module_name)
47 globals().update({k: v for k, v in module.__dict__.items() if not k.startswith('_')})
48
49
44 def ipython_shell_runner(env, help):
50 def ipython_shell_runner(env, help):
45
51
46 # imports, used in ipython shell
52 # imports, used in ipython shell
@@ -50,7 +56,7 b' def ipython_shell_runner(env, help):'
50 import shutil
56 import shutil
51 import datetime
57 import datetime
52 from rhodecode.model import user, user_group, repo, repo_group
58 from rhodecode.model import user, user_group, repo, repo_group
53 from rhodecode.model.db import *
59 import_all_from_module('rhodecode.model.db')
54
60
55 try:
61 try:
56 import IPython
62 import IPython
@@ -19,40 +19,35 b''
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import configparser
22
23 from pyramid.paster import bootstrap as pyramid_bootstrap, setup_logging # pragma: no cover
23 from pyramid.paster import bootstrap as pyramid_bootstrap, setup_logging # pragma: no cover
24
24 from pyramid.threadlocal import get_current_request as pyramid_current_request
25 from rhodecode.lib.request import Request
26
27
28 def get_config(ini_path, **kwargs):
29 parser = configparser.ConfigParser(**kwargs)
30 parser.read(ini_path)
31 return parser
32
33
34 def get_app_config(ini_path):
35 from paste.deploy.loadwsgi import appconfig
36 return appconfig(f'config:{ini_path}', relative_to=os.getcwd())
37
25
38
26
39 def bootstrap(config_uri, options=None, env=None):
27 def bootstrap(config_uri, options=None, env=None):
28 from rhodecode.config.utils import DEFAULT_USER
29 from rhodecode.lib.config_utils import get_app_config_lightweight
40 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.lib.utils2 import AttributeDict
31 from rhodecode.lib.request import Request
41
32
42 if env:
33 if env:
43 os.environ.update(env)
34 os.environ.update(env)
44
35
45 config = get_config(config_uri)
36 config = get_app_config_lightweight(config_uri)
46 base_url = 'http://rhodecode.local'
37 base_url = config['app.base_url']
47 try:
48 base_url = config.get('app:main', 'app.base_url')
49 except (configparser.NoSectionError, configparser.NoOptionError):
50 pass
51
38
52 request = Request.blank('/', base_url=base_url)
39 request = Request.blank('/', base_url=base_url)
53 # fake inject a running user for bootstrap request !
40 # fake inject a running user for bootstrap request !
54 request.user = AttributeDict({'username': 'bootstrap-user',
41 request.user = AttributeDict({'username': DEFAULT_USER,
55 'user_id': 1,
42 'user_id': 1,
56 'ip_addr': '127.0.0.1'})
43 'ip_addr': '127.0.0.1'})
57 return pyramid_bootstrap(config_uri, request=request, options=options)
44 return pyramid_bootstrap(config_uri, request=request, options=options)
58
45
46
47 def get_current_request():
48 pyramid_req = pyramid_current_request()
49 if not pyramid_req:
50 # maybe we're in celery context and need to get the PYRAMID_REQUEST
51 from rhodecode.lib.celerylib.loader import celery_app
52 pyramid_req = celery_app.conf['PYRAMID_REQUEST']
53 return pyramid_req
@@ -20,7 +20,8 b' import logging'
20 import click
20 import click
21 import pyramid.paster
21 import pyramid.paster
22
22
23 from rhodecode.lib.pyramid_utils import bootstrap, get_app_config
23 from rhodecode.lib.pyramid_utils import bootstrap
24 from rhodecode.lib.config_utils import get_app_config
24 from rhodecode.lib.db_manage import DbManage
25 from rhodecode.lib.db_manage import DbManage
25 from rhodecode.lib.utils2 import get_encryption_key
26 from rhodecode.lib.utils2 import get_encryption_key
26 from rhodecode.model.db import Session
27 from rhodecode.model.db import Session
@@ -25,6 +25,9 b' class StatsdClientClass(Singleton):'
25 statsd_client = None
25 statsd_client = None
26 statsd = None
26 statsd = None
27
27
28 def __repr__(self):
29 return f"{self.__class__}(statsd={self.statsd})"
30
28 def __getattribute__(self, name):
31 def __getattribute__(self, name):
29
32
30 if name.startswith("statsd"):
33 if name.startswith("statsd"):
@@ -167,3 +167,17 b' def convert_special_chars(str_) -> str:'
167 value = safe_str(str_)
167 value = safe_str(str_)
168 converted_value = unidecode(value)
168 converted_value = unidecode(value)
169 return converted_value
169 return converted_value
170
171
172 def splitnewlines(text: bytes):
173 """
174 like splitlines, but only split on newlines.
175 """
176
177 lines = [_l + b'\n' for _l in text.split(b'\n')]
178 if lines:
179 if lines[-1] == b'\n':
180 lines.pop()
181 else:
182 lines[-1] = lines[-1][:-1]
183 return lines
@@ -100,7 +100,7 b' def get_cert_path(ini_path):'
100 default = '/etc/ssl/certs/ca-certificates.crt'
100 default = '/etc/ssl/certs/ca-certificates.crt'
101 control_ca_bundle = os.path.join(
101 control_ca_bundle = os.path.join(
102 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(ini_path)))),
102 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(ini_path)))),
103 '.rccontrol-profile/etc/ca-bundle.crt')
103 '/etc/ssl/certs/ca-certificates.crt')
104 if os.path.isfile(control_ca_bundle):
104 if os.path.isfile(control_ca_bundle):
105 default = control_ca_bundle
105 default = control_ca_bundle
106
106
@@ -323,7 +323,7 b' def cpu():'
323 value['cpu_count'] = psutil.cpu_count()
323 value['cpu_count'] = psutil.cpu_count()
324
324
325 human_value = value.copy()
325 human_value = value.copy()
326 human_value['text'] = '{} cores at {} %'.format(value['cpu_count'], value['cpu'])
326 human_value['text'] = f'{value["cpu_count"]} cores at {value["cpu"]} %'
327
327
328 return SysInfoRes(value=value, state=state, human_value=human_value)
328 return SysInfoRes(value=value, state=state, human_value=human_value)
329
329
@@ -331,8 +331,8 b' def cpu():'
331 @register_sysinfo
331 @register_sysinfo
332 def storage():
332 def storage():
333 from rhodecode.lib.helpers import format_byte_size_binary
333 from rhodecode.lib.helpers import format_byte_size_binary
334 from rhodecode.model.settings import VcsSettingsModel
334 from rhodecode.lib.utils import get_rhodecode_repo_store_path
335 path = VcsSettingsModel().get_repos_location()
335 path = get_rhodecode_repo_store_path()
336
336
337 value = dict(percent=0, used=0, total=0, path=path, text='')
337 value = dict(percent=0, used=0, total=0, path=path, text='')
338 state = STATE_OK_DEFAULT
338 state = STATE_OK_DEFAULT
@@ -364,8 +364,8 b' def storage():'
364
364
365 @register_sysinfo
365 @register_sysinfo
366 def storage_inodes():
366 def storage_inodes():
367 from rhodecode.model.settings import VcsSettingsModel
367 from rhodecode.lib.utils import get_rhodecode_repo_store_path
368 path = VcsSettingsModel().get_repos_location()
368 path = get_rhodecode_repo_store_path()
369
369
370 value = dict(percent=0.0, free=0, used=0, total=0, path=path, text='')
370 value = dict(percent=0.0, free=0, used=0, total=0, path=path, text='')
371 state = STATE_OK_DEFAULT
371 state = STATE_OK_DEFAULT
@@ -398,32 +398,24 b' def storage_inodes():'
398 @register_sysinfo
398 @register_sysinfo
399 def storage_archives():
399 def storage_archives():
400 import rhodecode
400 import rhodecode
401 from rhodecode.lib.utils import safe_str
402 from rhodecode.lib.helpers import format_byte_size_binary
401 from rhodecode.lib.helpers import format_byte_size_binary
402 from rhodecode.lib.archive_cache import get_archival_cache_store
403
403
404 msg = 'Archive cache storage is controlled by ' \
404 storage_type = rhodecode.ConfigGet().get_str('archive_cache.backend.type')
405 'archive_cache.store_dir=/path/to/cache option in the .ini file'
406 path = safe_str(rhodecode.CONFIG.get('archive_cache.store_dir', msg))
407
405
408 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
406 value = dict(percent=0, used=0, total=0, items=0, path='', text='', type=storage_type)
409 state = STATE_OK_DEFAULT
407 state = STATE_OK_DEFAULT
410 try:
408 try:
411 items_count = 0
409 d_cache = get_archival_cache_store(config=rhodecode.CONFIG)
412 used = 0
413 for root, dirs, files in os.walk(path):
414 if root == path:
415 items_count = len(dirs)
416
410
417 for f in files:
411 total_files, total_size, _directory_stats = d_cache.get_statistics()
418 try:
412
419 used += os.path.getsize(os.path.join(root, f))
420 except OSError:
421 pass
422 value.update({
413 value.update({
423 'percent': 100,
414 'percent': 100,
424 'used': used,
415 'used': total_size,
425 'total': used,
416 'total': total_size,
426 'items': items_count
417 'items': total_files,
418 'path': d_cache.storage_path
427 })
419 })
428
420
429 except Exception as e:
421 except Exception as e:
@@ -442,33 +434,23 b' def storage_archives():'
442 @register_sysinfo
434 @register_sysinfo
443 def storage_gist():
435 def storage_gist():
444 from rhodecode.model.gist import GIST_STORE_LOC
436 from rhodecode.model.gist import GIST_STORE_LOC
445 from rhodecode.model.settings import VcsSettingsModel
437 from rhodecode.lib.utils import safe_str, get_rhodecode_repo_store_path
446 from rhodecode.lib.utils import safe_str
438 from rhodecode.lib.helpers import format_byte_size_binary, get_directory_statistics
447 from rhodecode.lib.helpers import format_byte_size_binary
439
448 path = safe_str(os.path.join(
440 path = safe_str(os.path.join(
449 VcsSettingsModel().get_repos_location(), GIST_STORE_LOC))
441 get_rhodecode_repo_store_path(), GIST_STORE_LOC))
450
442
451 # gist storage
443 # gist storage
452 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
444 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
453 state = STATE_OK_DEFAULT
445 state = STATE_OK_DEFAULT
454
446
455 try:
447 try:
456 items_count = 0
448 total_files, total_size, _directory_stats = get_directory_statistics(path)
457 used = 0
458 for root, dirs, files in os.walk(path):
459 if root == path:
460 items_count = len(dirs)
461
462 for f in files:
463 try:
464 used += os.path.getsize(os.path.join(root, f))
465 except OSError:
466 pass
467 value.update({
449 value.update({
468 'percent': 100,
450 'percent': 100,
469 'used': used,
451 'used': total_size,
470 'total': used,
452 'total': total_size,
471 'items': items_count
453 'items': total_files
472 })
454 })
473 except Exception as e:
455 except Exception as e:
474 log.exception('failed to fetch gist storage items')
456 log.exception('failed to fetch gist storage items')
@@ -21,6 +21,7 b' Utilities library for RhodeCode'
21 """
21 """
22
22
23 import datetime
23 import datetime
24
24 import decorator
25 import decorator
25 import logging
26 import logging
26 import os
27 import os
@@ -31,7 +32,7 b' import socket'
31 import tempfile
32 import tempfile
32 import traceback
33 import traceback
33 import tarfile
34 import tarfile
34 import warnings
35
35 from functools import wraps
36 from functools import wraps
36 from os.path import join as jn
37 from os.path import join as jn
37
38
@@ -471,14 +472,14 b' def get_rhodecode_realm():'
471 return safe_str(realm.app_settings_value)
472 return safe_str(realm.app_settings_value)
472
473
473
474
474 def get_rhodecode_base_path():
475 def get_rhodecode_repo_store_path():
475 """
476 """
476 Returns the base path. The base path is the filesystem path which points
477 Returns the base path. The base path is the filesystem path which points
477 to the repository store.
478 to the repository store.
478 """
479 """
479
480
480 import rhodecode
481 import rhodecode
481 return rhodecode.CONFIG['default_base_path']
482 return rhodecode.CONFIG['repo_store.path']
482
483
483
484
484 def map_groups(path):
485 def map_groups(path):
@@ -33,7 +33,8 b' from rhodecode.lib.vcs.exceptions import'
33
33
34 __all__ = [
34 __all__ = [
35 'get_vcs_instance', 'get_backend',
35 'get_vcs_instance', 'get_backend',
36 'VCSError', 'RepositoryError', 'CommitError', 'VCSCommunicationError'
36 'VCSError', 'RepositoryError', 'CommitError', 'VCSCommunicationError',
37 'CurlSession', 'CurlResponse'
37 ]
38 ]
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
@@ -135,7 +136,12 b' class CurlSession(object):'
135 curl.setopt(curl.FOLLOWLOCATION, allow_redirects)
136 curl.setopt(curl.FOLLOWLOCATION, allow_redirects)
136 curl.setopt(curl.WRITEDATA, response_buffer)
137 curl.setopt(curl.WRITEDATA, response_buffer)
137 curl.setopt(curl.HTTPHEADER, headers_list)
138 curl.setopt(curl.HTTPHEADER, headers_list)
139
140 try:
138 curl.perform()
141 curl.perform()
142 except pycurl.error as exc:
143 log.error('Failed to call endpoint url: {} using pycurl'.format(url))
144 raise
139
145
140 status_code = curl.getinfo(pycurl.HTTP_CODE)
146 status_code = curl.getinfo(pycurl.HTTP_CODE)
141 content_type = curl.getinfo(pycurl.CONTENT_TYPE)
147 content_type = curl.getinfo(pycurl.CONTENT_TYPE)
@@ -326,6 +326,9 b' class GitRepository(BaseRepository):'
326 def _get_branches(self):
326 def _get_branches(self):
327 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
327 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
328
328
329 def delete_branch(self, branch_name):
330 return self._remote.delete_branch(branch_name)
331
329 @CachedProperty
332 @CachedProperty
330 def branches(self):
333 def branches(self):
331 return self._get_branches()
334 return self._get_branches()
@@ -1037,6 +1040,8 b' class GitRepository(BaseRepository):'
1037 pr_branch, self.path, target_ref.name, enable_hooks=True,
1040 pr_branch, self.path, target_ref.name, enable_hooks=True,
1038 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
1041 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
1039 merge_succeeded = True
1042 merge_succeeded = True
1043 if close_branch and source_ref.name != target_ref.name and not dry_run and source_ref.type == 'branch':
1044 self.delete_branch(source_ref.name)
1040 except RepositoryError:
1045 except RepositoryError:
1041 log.exception(
1046 log.exception(
1042 'Failure when doing local push from the shadow '
1047 'Failure when doing local push from the shadow '
@@ -35,6 +35,7 b' from rhodecode.lib.datelib import ('
35 from rhodecode.lib.str_utils import safe_str
35 from rhodecode.lib.str_utils import safe_str
36 from rhodecode.lib.utils2 import CachedProperty
36 from rhodecode.lib.utils2 import CachedProperty
37 from rhodecode.lib.vcs import connection, exceptions
37 from rhodecode.lib.vcs import connection, exceptions
38 from rhodecode.lib.vcs.conf import settings as vcs_settings
38 from rhodecode.lib.vcs.backends.base import (
39 from rhodecode.lib.vcs.backends.base import (
39 BaseRepository, CollectionGenerator, Config, MergeResponse,
40 BaseRepository, CollectionGenerator, Config, MergeResponse,
40 MergeFailureReason, Reference, BasePathPermissionChecker)
41 MergeFailureReason, Reference, BasePathPermissionChecker)
@@ -722,7 +723,12 b' class MercurialRepository(BaseRepository'
722 commit needs to be pushed.
723 commit needs to be pushed.
723 """
724 """
724 self._update(source_ref.commit_id)
725 self._update(source_ref.commit_id)
725 message = close_message or f"Closing branch: `{source_ref.name}`"
726 message = (close_message or vcs_settings.HG_CLOSE_BRANCH_MESSAGE_TMPL).format(
727 user_name=user_name,
728 user_email=user_email,
729 target_ref_name=target_ref.name,
730 source_ref_name=source_ref.name
731 )
726 try:
732 try:
727 self._remote.commit(
733 self._remote.commit(
728 message=safe_str(message),
734 message=safe_str(message),
@@ -58,6 +58,9 b' MERGE_MESSAGE_TMPL = ('
58 MERGE_DRY_RUN_MESSAGE = 'dry_run_merge_message_from_rhodecode'
58 MERGE_DRY_RUN_MESSAGE = 'dry_run_merge_message_from_rhodecode'
59 MERGE_DRY_RUN_USER = 'Dry-Run User'
59 MERGE_DRY_RUN_USER = 'Dry-Run User'
60 MERGE_DRY_RUN_EMAIL = 'dry-run-merge@rhodecode.com'
60 MERGE_DRY_RUN_EMAIL = 'dry-run-merge@rhodecode.com'
61 HG_CLOSE_BRANCH_MESSAGE_TMPL = (
62 'Closing branch: `{source_ref_name}`'
63 )
61
64
62
65
63 def available_aliases():
66 def available_aliases():
@@ -146,6 +146,10 b' class CommandError(VCSError):'
146 pass
146 pass
147
147
148
148
149 class ImproperlyConfiguredError(Exception):
150 pass
151
152
149 class UnhandledException(VCSError):
153 class UnhandledException(VCSError):
150 """
154 """
151 Signals that something unexpected went wrong.
155 Signals that something unexpected went wrong.
@@ -21,6 +21,7 b' import logging'
21
21
22 import rhodecode
22 import rhodecode
23 from rhodecode.model import meta, db
23 from rhodecode.model import meta, db
24 from rhodecode.lib.utils import get_rhodecode_repo_store_path
24 from rhodecode.lib.utils2 import obfuscate_url_pw, get_encryption_key
25 from rhodecode.lib.utils2 import obfuscate_url_pw, get_encryption_key
25
26
26 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
@@ -138,3 +139,11 b' class BaseModel(object):'
138 Returns all instances of what is defined in `cls` class variable
139 Returns all instances of what is defined in `cls` class variable
139 """
140 """
140 return cls.cls.getAll()
141 return cls.cls.getAll()
142
143 @property
144 def repos_path(self):
145 """
146 Gets the repositories root path from *ini file
147 """
148
149 return get_rhodecode_repo_store_path()
@@ -33,9 +33,10 b' import functools'
33 import traceback
33 import traceback
34 import collections
34 import collections
35
35
36 import pyotp
36 from sqlalchemy import (
37 from sqlalchemy import (
37 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 true, false, null,
39 true, false, null, union_all,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType, BigInteger)
42 Text, Float, PickleType, BigInteger)
@@ -51,6 +52,7 b' from zope.cachedescriptors.property impo'
51 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
52 from webhelpers2.text import remove_formatting
53 from webhelpers2.text import remove_formatting
53
54
55 from rhodecode import ConfigGet
54 from rhodecode.lib.str_utils import safe_bytes
56 from rhodecode.lib.str_utils import safe_bytes
55 from rhodecode.translation import _
57 from rhodecode.translation import _
56 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
@@ -126,6 +128,11 b' def _hash_key(k):'
126 return sha1_safe(k)
128 return sha1_safe(k)
127
129
128
130
131 def description_escaper(desc):
132 from rhodecode.lib import helpers as h
133 return h.escape(desc)
134
135
129 def in_filter_generator(qry, items, limit=500):
136 def in_filter_generator(qry, items, limit=500):
130 """
137 """
131 Splits IN() into multiple with OR
138 Splits IN() into multiple with OR
@@ -197,9 +204,7 b' class EncryptedTextValue(TypeDecorator):'
197 if not value:
204 if not value:
198 return value
205 return value
199
206
200 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
207 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY)
201
202 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
203
208
204 return safe_str(bytes_val)
209 return safe_str(bytes_val)
205
210
@@ -586,6 +591,7 b' class User(Base, BaseModel):'
586 DEFAULT_USER = 'default'
591 DEFAULT_USER = 'default'
587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
592 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
593 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
594 RECOVERY_CODES_COUNT = 10
589
595
590 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
596 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 username = Column("username", String(255), nullable=True, unique=None, default=None)
597 username = Column("username", String(255), nullable=True, unique=None, default=None)
@@ -662,16 +668,14 b' class User(Base, BaseModel):'
662
668
663 @hybrid_property
669 @hybrid_property
664 def first_name(self):
670 def first_name(self):
665 from rhodecode.lib import helpers as h
666 if self.name:
671 if self.name:
667 return h.escape(self.name)
672 return description_escaper(self.name)
668 return self.name
673 return self.name
669
674
670 @hybrid_property
675 @hybrid_property
671 def last_name(self):
676 def last_name(self):
672 from rhodecode.lib import helpers as h
673 if self.lastname:
677 if self.lastname:
674 return h.escape(self.lastname)
678 return description_escaper(self.lastname)
675 return self.lastname
679 return self.lastname
676
680
677 @hybrid_property
681 @hybrid_property
@@ -793,16 +797,148 b' class User(Base, BaseModel):'
793 Session.commit()
797 Session.commit()
794 return artifact_token.api_key
798 return artifact_token.api_key
795
799
796 @classmethod
800 def is_totp_valid(self, received_code, secret):
797 def get(cls, user_id, cache=False):
801 totp = pyotp.TOTP(secret)
798 if not user_id:
802 return totp.verify(received_code)
799 return
803
800
804 def is_2fa_recovery_code_valid(self, received_code, secret):
801 user = cls.query()
805 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
802 if cache:
806 recovery_codes = self.get_2fa_recovery_codes()
803 user = user.options(
807 if received_code in recovery_codes:
804 FromCache("sql_cache_short", f"get_users_{user_id}"))
808 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
805 return user.get(user_id)
809 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
810 return True
811 return False
812
813 @hybrid_property
814 def has_forced_2fa(self):
815 """
816 Checks if 2fa was forced for current user
817 """
818 from rhodecode.model.settings import SettingsModel
819 if value := SettingsModel().get_setting_by_name(f'auth_{self.extern_type}_global_2fa'):
820 return value.app_settings_value
821 return False
822
823 @hybrid_property
824 def has_enabled_2fa(self):
825 """
826 Checks if user enabled 2fa
827 """
828 if value := self.has_forced_2fa:
829 return value
830 return self.user_data.get('enabled_2fa', False)
831
832 @has_enabled_2fa.setter
833 def has_enabled_2fa(self, val):
834 val = str2bool(val)
835 self.update_userdata(enabled_2fa=val)
836 if not val:
837 # NOTE: setting to false we clear the user_data to not store any 2fa artifacts
838 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False)
839 Session().commit()
840
841 @hybrid_property
842 def check_2fa_required(self):
843 """
844 Check if check 2fa flag is set for this user
845 """
846 value = self.user_data.get('check_2fa', False)
847 return value
848
849 @check_2fa_required.setter
850 def check_2fa_required(self, val):
851 val = str2bool(val)
852 self.update_userdata(check_2fa=val)
853 Session().commit()
854
855 @hybrid_property
856 def has_seen_2fa_codes(self):
857 """
858 get the flag about if user has seen 2fa recovery codes
859 """
860 value = self.user_data.get('recovery_codes_2fa_seen', False)
861 return value
862
863 @has_seen_2fa_codes.setter
864 def has_seen_2fa_codes(self, val):
865 val = str2bool(val)
866 self.update_userdata(recovery_codes_2fa_seen=val)
867 Session().commit()
868
869 @hybrid_property
870 def needs_2fa_configure(self):
871 """
872 Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work.
873
874 Currently this is 2fa enabled and secret exists
875 """
876 if self.has_enabled_2fa:
877 return not self.user_data.get('secret_2fa')
878 return False
879
880 def init_2fa_recovery_codes(self, persist=True, force=False):
881 """
882 Creates 2fa recovery codes
883 """
884 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
885 encrypted_codes = []
886 if not recovery_codes or force:
887 for _ in range(self.RECOVERY_CODES_COUNT):
888 recovery_code = pyotp.random_base32()
889 recovery_codes.append(recovery_code)
890 encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY)
891 encrypted_codes.append(safe_str(encrypted_code))
892 if persist:
893 self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False)
894 return recovery_codes
895 # User should not check the same recovery codes more than once
896 return []
897
898 def get_2fa_recovery_codes(self):
899 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
900
901 recovery_codes = list(map(
902 lambda val: safe_str(
903 enc_utils.decrypt_value(
904 val,
905 enc_key=ENCRYPTION_KEY
906 )),
907 encrypted_recovery_codes))
908 return recovery_codes
909
910 def init_secret_2fa(self, persist=True, force=False):
911 secret_2fa = self.user_data.get('secret_2fa')
912 if not secret_2fa or force:
913 secret = pyotp.random_base32()
914 if persist:
915 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY)))
916 return secret
917 return ''
918
919 @hybrid_property
920 def secret_2fa(self) -> str:
921 """
922 get stored secret for 2fa
923 """
924 secret_2fa = self.user_data.get('secret_2fa')
925 if secret_2fa:
926 return safe_str(
927 enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY))
928 return ''
929
930 @secret_2fa.setter
931 def secret_2fa(self, value: str) -> None:
932 encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY)
933 self.update_userdata(secret_2fa=safe_str(encrypted_value))
934
935 def regenerate_2fa_recovery_codes(self):
936 """
937 Regenerates 2fa recovery codes upon request
938 """
939 new_recovery_codes = self.init_2fa_recovery_codes(force=True)
940 Session().commit()
941 return new_recovery_codes
806
942
807 @classmethod
943 @classmethod
808 def extra_valid_auth_tokens(cls, user, role=None):
944 def extra_valid_auth_tokens(cls, user, role=None):
@@ -930,13 +1066,24 b' class User(Base, BaseModel):'
930 @user_data.setter
1066 @user_data.setter
931 def user_data(self, val):
1067 def user_data(self, val):
932 if not isinstance(val, dict):
1068 if not isinstance(val, dict):
933 raise Exception('user_data must be dict, got %s' % type(val))
1069 raise Exception(f'user_data must be dict, got {type(val)}')
934 try:
1070 try:
935 self._user_data = safe_bytes(json.dumps(val))
1071 self._user_data = safe_bytes(json.dumps(val))
936 except Exception:
1072 except Exception:
937 log.error(traceback.format_exc())
1073 log.error(traceback.format_exc())
938
1074
939 @classmethod
1075 @classmethod
1076 def get(cls, user_id, cache=False):
1077 if not user_id:
1078 return
1079
1080 user = cls.query()
1081 if cache:
1082 user = user.options(
1083 FromCache("sql_cache_short", f"get_users_{user_id}"))
1084 return user.get(user_id)
1085
1086 @classmethod
940 def get_by_username(cls, username, case_insensitive=False,
1087 def get_by_username(cls, username, case_insensitive=False,
941 cache=False):
1088 cache=False):
942
1089
@@ -954,6 +1101,12 b' class User(Base, BaseModel):'
954 return cls.execute(q).scalar_one_or_none()
1101 return cls.execute(q).scalar_one_or_none()
955
1102
956 @classmethod
1103 @classmethod
1104 def get_by_username_or_primary_email(cls, user_identifier):
1105 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1106 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1107 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1108
1109 @classmethod
957 def get_by_auth_token(cls, auth_token, cache=False):
1110 def get_by_auth_token(cls, auth_token, cache=False):
958
1111
959 q = cls.select(User)\
1112 q = cls.select(User)\
@@ -1218,8 +1371,7 b' class UserApiKeys(Base, BaseModel):'
1218
1371
1219 @hybrid_property
1372 @hybrid_property
1220 def description_safe(self):
1373 def description_safe(self):
1221 from rhodecode.lib import helpers as h
1374 return description_escaper(self.description)
1222 return h.escape(self.description)
1223
1375
1224 @property
1376 @property
1225 def expired(self):
1377 def expired(self):
@@ -1322,8 +1474,7 b' class UserIpMap(Base, BaseModel):'
1322
1474
1323 @hybrid_property
1475 @hybrid_property
1324 def description_safe(self):
1476 def description_safe(self):
1325 from rhodecode.lib import helpers as h
1477 return description_escaper(self.description)
1326 return h.escape(self.description)
1327
1478
1328 @classmethod
1479 @classmethod
1329 def _get_ip_range(cls, ip_addr):
1480 def _get_ip_range(cls, ip_addr):
@@ -1461,8 +1612,7 b' class UserGroup(Base, BaseModel):'
1461
1612
1462 @hybrid_property
1613 @hybrid_property
1463 def description_safe(self):
1614 def description_safe(self):
1464 from rhodecode.lib import helpers as h
1615 return description_escaper(self.user_group_description)
1465 return h.escape(self.user_group_description)
1466
1616
1467 @hybrid_property
1617 @hybrid_property
1468 def group_data(self):
1618 def group_data(self):
@@ -1514,7 +1664,7 b' class UserGroup(Base, BaseModel):'
1514 user_group = cls.query()
1664 user_group = cls.query()
1515 if cache:
1665 if cache:
1516 user_group = user_group.options(
1666 user_group = user_group.options(
1517 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1667 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1518 return user_group.get(user_group_id)
1668 return user_group.get(user_group_id)
1519
1669
1520 def permissions(self, with_admins=True, with_owner=True,
1670 def permissions(self, with_admins=True, with_owner=True,
@@ -1806,8 +1956,7 b' class Repository(Base, BaseModel):'
1806
1956
1807 @hybrid_property
1957 @hybrid_property
1808 def description_safe(self):
1958 def description_safe(self):
1809 from rhodecode.lib import helpers as h
1959 return description_escaper(self.description)
1810 return h.escape(self.description)
1811
1960
1812 @hybrid_property
1961 @hybrid_property
1813 def landing_rev(self):
1962 def landing_rev(self):
@@ -1908,7 +2057,7 b' class Repository(Base, BaseModel):'
1908 if val:
2057 if val:
1909 return val
2058 return val
1910 else:
2059 else:
1911 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
2060 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
1912 q = q.options(
2061 q = q.options(
1913 FromCache("sql_cache_short", cache_key))
2062 FromCache("sql_cache_short", cache_key))
1914
2063
@@ -1942,8 +2091,8 b' class Repository(Base, BaseModel):'
1942
2091
1943 :param cls:
2092 :param cls:
1944 """
2093 """
1945 from rhodecode.lib.utils import get_rhodecode_base_path
2094 from rhodecode.lib.utils import get_rhodecode_repo_store_path
1946 return get_rhodecode_base_path()
2095 return get_rhodecode_repo_store_path()
1947
2096
1948 @classmethod
2097 @classmethod
1949 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2098 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
@@ -2009,16 +2158,13 b' class Repository(Base, BaseModel):'
2009 def groups_and_repo(self):
2158 def groups_and_repo(self):
2010 return self.groups_with_parents, self
2159 return self.groups_with_parents, self
2011
2160
2012 @LazyProperty
2161 @property
2013 def repo_path(self):
2162 def repo_path(self):
2014 """
2163 """
2015 Returns base full path for that repository means where it actually
2164 Returns base full path for that repository means where it actually
2016 exists on a filesystem
2165 exists on a filesystem
2017 """
2166 """
2018 q = Session().query(RhodeCodeUi).filter(
2167 return self.base_path()
2019 RhodeCodeUi.ui_key == self.NAME_SEP)
2020 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2021 return q.one().ui_value
2022
2168
2023 @property
2169 @property
2024 def repo_full_path(self):
2170 def repo_full_path(self):
@@ -2768,8 +2914,7 b' class RepoGroup(Base, BaseModel):'
2768
2914
2769 @hybrid_property
2915 @hybrid_property
2770 def description_safe(self):
2916 def description_safe(self):
2771 from rhodecode.lib import helpers as h
2917 return description_escaper(self.group_description)
2772 return h.escape(self.group_description)
2773
2918
2774 @classmethod
2919 @classmethod
2775 def hash_repo_group_name(cls, repo_group_name):
2920 def hash_repo_group_name(cls, repo_group_name):
@@ -4271,8 +4416,7 b' class _PullRequestBase(BaseModel):'
4271
4416
4272 @hybrid_property
4417 @hybrid_property
4273 def description_safe(self):
4418 def description_safe(self):
4274 from rhodecode.lib import helpers as h
4419 return description_escaper(self.description)
4275 return h.escape(self.description)
4276
4420
4277 @hybrid_property
4421 @hybrid_property
4278 def revisions(self):
4422 def revisions(self):
@@ -4438,6 +4582,12 b' class PullRequest(Base, _PullRequestBase'
4438 else:
4582 else:
4439 return f'<DB:PullRequest at {id(self)!r}>'
4583 return f'<DB:PullRequest at {id(self)!r}>'
4440
4584
4585 def __str__(self):
4586 if self.pull_request_id:
4587 return f'#{self.pull_request_id}'
4588 else:
4589 return f'#{id(self)!r}'
4590
4441 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4591 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4442 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4592 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4443 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4593 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
@@ -4874,8 +5024,7 b' class Gist(Base, BaseModel):'
4874
5024
4875 @hybrid_property
5025 @hybrid_property
4876 def description_safe(self):
5026 def description_safe(self):
4877 from rhodecode.lib import helpers as h
5027 return description_escaper(self.gist_description)
4878 return h.escape(self.gist_description)
4879
5028
4880 @classmethod
5029 @classmethod
4881 def get_or_404(cls, id_):
5030 def get_or_404(cls, id_):
@@ -4903,10 +5052,9 b' class Gist(Base, BaseModel):'
4903 :param cls:
5052 :param cls:
4904 """
5053 """
4905 from rhodecode.model.gist import GIST_STORE_LOC
5054 from rhodecode.model.gist import GIST_STORE_LOC
4906 q = Session().query(RhodeCodeUi)\
5055 from rhodecode.lib.utils import get_rhodecode_repo_store_path
4907 .filter(RhodeCodeUi.ui_key == URL_SEP)
5056 repo_store_path = get_rhodecode_repo_store_path()
4908 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
5057 return os.path.join(repo_store_path, GIST_STORE_LOC)
4909 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4910
5058
4911 def get_api_data(self):
5059 def get_api_data(self):
4912 """
5060 """
@@ -4928,8 +5076,7 b' class Gist(Base, BaseModel):'
4928 return data
5076 return data
4929
5077
4930 def __json__(self):
5078 def __json__(self):
4931 data = dict(
5079 data = dict()
4932 )
4933 data.update(self.get_api_data())
5080 data.update(self.get_api_data())
4934 return data
5081 return data
4935 # SCM functions
5082 # SCM functions
@@ -5334,8 +5481,7 b' class ScheduleEntry(Base, BaseModel):'
5334 @schedule_type.setter
5481 @schedule_type.setter
5335 def schedule_type(self, val):
5482 def schedule_type(self, val):
5336 if val not in self.schedule_types:
5483 if val not in self.schedule_types:
5337 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5484 raise ValueError(f'Value must be on of `{val}` and got `{self.schedule_type}`')
5338 val, self.schedule_type))
5339
5485
5340 self._schedule_type = val
5486 self._schedule_type = val
5341
5487
@@ -5343,21 +5489,25 b' class ScheduleEntry(Base, BaseModel):'
5343 def get_uid(cls, obj):
5489 def get_uid(cls, obj):
5344 args = obj.task_args
5490 args = obj.task_args
5345 kwargs = obj.task_kwargs
5491 kwargs = obj.task_kwargs
5492
5346 if isinstance(args, JsonRaw):
5493 if isinstance(args, JsonRaw):
5347 try:
5494 try:
5348 args = json.loads(args)
5495 args = json.loads(str(args))
5349 except ValueError:
5496 except ValueError:
5497 log.exception('json.loads of args failed...')
5350 args = tuple()
5498 args = tuple()
5351
5499
5352 if isinstance(kwargs, JsonRaw):
5500 if isinstance(kwargs, JsonRaw):
5353 try:
5501 try:
5354 kwargs = json.loads(kwargs)
5502 kwargs = json.loads(str(kwargs))
5355 except ValueError:
5503 except ValueError:
5504 log.exception('json.loads of kwargs failed...')
5356 kwargs = dict()
5505 kwargs = dict()
5357
5506
5358 dot_notation = obj.task_dot_notation
5507 dot_notation = obj.task_dot_notation
5359 val = '.'.join(map(safe_str, [
5508 val = '.'.join(map(safe_str, [dot_notation, args, sorted(kwargs.items())]))
5360 sorted(dot_notation), args, sorted(kwargs.items())]))
5509 log.debug('calculating task uid using id:`%s`', val)
5510
5361 return sha1(safe_bytes(val))
5511 return sha1(safe_bytes(val))
5362
5512
5363 @classmethod
5513 @classmethod
@@ -5368,6 +5518,10 b' class ScheduleEntry(Base, BaseModel):'
5368 def get_by_schedule_id(cls, schedule_id):
5518 def get_by_schedule_id(cls, schedule_id):
5369 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5519 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5370
5520
5521 @classmethod
5522 def get_by_task_uid(cls, task_uid):
5523 return cls.query().filter(cls.task_uid == task_uid).scalar()
5524
5371 @property
5525 @property
5372 def task(self):
5526 def task(self):
5373 return self.task_dot_notation
5527 return self.task_dot_notation
@@ -5549,18 +5703,23 b' class UserBookmark(Base, BaseModel):'
5549
5703
5550 @classmethod
5704 @classmethod
5551 def get_bookmarks_for_user(cls, user_id, cache=True):
5705 def get_bookmarks_for_user(cls, user_id, cache=True):
5552 bookmarks = cls.query() \
5706 bookmarks = select(
5553 .filter(UserBookmark.user_id == user_id) \
5707 UserBookmark.title,
5554 .options(joinedload(UserBookmark.repository)) \
5708 UserBookmark.position,
5555 .options(joinedload(UserBookmark.repository_group)) \
5709 ) \
5710 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5711 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5712 .where(UserBookmark.user_id == user_id) \
5713 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5714 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5556 .order_by(UserBookmark.position.asc())
5715 .order_by(UserBookmark.position.asc())
5557
5716
5558 if cache:
5717 if cache:
5559 bookmarks = bookmarks.options(
5718 bookmarks = bookmarks.options(
5560 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5719 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5561 )
5720 )
5562
5721
5563 return bookmarks.all()
5722 return Session().execute(bookmarks).all()
5564
5723
5565 def __repr__(self):
5724 def __repr__(self):
5566 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5725 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
@@ -104,6 +104,31 b' def LoginForm(localizer):'
104 return _LoginForm
104 return _LoginForm
105
105
106
106
107 def TOTPForm(localizer, user, allow_recovery_code_use=False):
108 _ = localizer
109
110 class _TOTPForm(formencode.Schema):
111 allow_extra_fields = True
112 filter_extra_fields = False
113 totp = v.Regex(r'^(?:\d{6}|[A-Z0-9]{32})$')
114 secret_totp = v.String()
115
116 def to_python(self, value, state=None):
117 validation_checks = [user.is_totp_valid]
118 if allow_recovery_code_use:
119 validation_checks.append(user.is_2fa_recovery_code_valid)
120 form_data = super().to_python(value, state)
121 received_code = form_data['totp']
122 secret = form_data.get('secret_totp')
123
124 if not any(map(lambda func: func(received_code, secret), validation_checks)):
125 error_msg = _('Code is invalid. Try again!')
126 raise formencode.Invalid(error_msg, v, state, error_dict={'totp': error_msg})
127 return form_data
128
129 return _TOTPForm
130
131
107 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
132 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
108 old_data = old_data or {}
133 old_data = old_data or {}
109 available_languages = available_languages or []
134 available_languages = available_languages or []
@@ -421,10 +446,6 b' class _BaseVcsSettingsForm(formencode.Sc'
421 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
446 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
422 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
447 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
423
448
424 # svn
425 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
426 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
427
428 # cache
449 # cache
429 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
450 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
430
451
@@ -434,10 +455,6 b' def ApplicationUiSettingsForm(localizer)'
434
455
435 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
456 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
436 web_push_ssl = v.StringBoolean(if_missing=False)
457 web_push_ssl = v.StringBoolean(if_missing=False)
437 paths_root_path = All(
438 v.ValidPath(localizer),
439 v.UnicodeString(strip=True, min=1, not_empty=True)
440 )
441 largefiles_usercache = All(
458 largefiles_usercache = All(
442 v.ValidPath(localizer),
459 v.ValidPath(localizer),
443 v.UnicodeString(strip=True, min=2, not_empty=True))
460 v.UnicodeString(strip=True, min=2, not_empty=True))
@@ -38,7 +38,7 b' from rhodecode.translation import lazy_u'
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from collections import OrderedDict
40 from collections import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
42 from rhodecode.lib.ext_json import sjson as json
42 from rhodecode.lib.ext_json import sjson as json
43 from rhodecode.lib.markup_renderer import (
43 from rhodecode.lib.markup_renderer import (
44 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
@@ -83,14 +83,6 b' class RepoModel(BaseModel):'
83
83
84 return repo_to_perm
84 return repo_to_perm
85
85
86 @LazyProperty
87 def repos_path(self):
88 """
89 Gets the repositories root path from database
90 """
91 settings_model = VcsSettingsModel(sa=self.sa)
92 return settings_model.get_repos_location()
93
94 def get(self, repo_id):
86 def get(self, repo_id):
95 repo = self.sa.query(Repository) \
87 repo = self.sa.query(Repository) \
96 .filter(Repository.repo_id == repo_id)
88 .filter(Repository.repo_id == repo_id)
@@ -608,7 +600,7 b' class RepoModel(BaseModel):'
608 # we need to flush here, in order to check if database won't
600 # we need to flush here, in order to check if database won't
609 # throw any exceptions, create filesystem dirs at the very end
601 # throw any exceptions, create filesystem dirs at the very end
610 self.sa.flush()
602 self.sa.flush()
611 events.trigger(events.RepoCreateEvent(new_repo))
603 events.trigger(events.RepoCreateEvent(new_repo, actor=owner))
612 return new_repo
604 return new_repo
613
605
614 except Exception:
606 except Exception:
@@ -62,15 +62,6 b' class RepoGroupModel(BaseModel):'
62 def get_repo_group(self, repo_group):
62 def get_repo_group(self, repo_group):
63 return self._get_repo_group(repo_group)
63 return self._get_repo_group(repo_group)
64
64
65 @LazyProperty
66 def repos_path(self):
67 """
68 Gets the repositories root path from database
69 """
70
71 settings_model = VcsSettingsModel(sa=self.sa)
72 return settings_model.get_repos_location()
73
74 def get_by_group_name(self, repo_group_name, cache=None):
65 def get_by_group_name(self, repo_group_name, cache=None):
75 repo = self.sa.query(RepoGroup) \
66 repo = self.sa.query(RepoGroup) \
76 .filter(RepoGroup.group_name == repo_group_name)
67 .filter(RepoGroup.group_name == repo_group_name)
@@ -189,15 +189,6 b' class ScmModel(BaseModel):'
189 Generic Scm Model
189 Generic Scm Model
190 """
190 """
191
191
192 @LazyProperty
193 def repos_path(self):
194 """
195 Gets the repositories root path from database
196 """
197
198 settings_model = VcsSettingsModel(sa=self.sa)
199 return settings_model.get_repos_location()
200
201 def repo_scan(self, repos_path=None):
192 def repo_scan(self, repos_path=None):
202 """
193 """
203 Listing of repositories in given path. This path should not be a
194 Listing of repositories in given path. This path should not be a
@@ -499,11 +499,6 b' class VcsSettingsModel(object):'
499 ('vcs_git_lfs', 'store_location')
499 ('vcs_git_lfs', 'store_location')
500 )
500 )
501
501
502 GLOBAL_SVN_SETTINGS = (
503 ('vcs_svn_proxy', 'http_requests_enabled'),
504 ('vcs_svn_proxy', 'http_server_url')
505 )
506
507 SVN_BRANCH_SECTION = 'vcs_svn_branch'
502 SVN_BRANCH_SECTION = 'vcs_svn_branch'
508 SVN_TAG_SECTION = 'vcs_svn_tag'
503 SVN_TAG_SECTION = 'vcs_svn_tag'
509 SSL_SETTING = ('web', 'push_ssl')
504 SSL_SETTING = ('web', 'push_ssl')
@@ -718,25 +713,10 b' class VcsSettingsModel(object):'
718 # branch/tags patterns
713 # branch/tags patterns
719 self._create_svn_settings(self.global_settings, data)
714 self._create_svn_settings(self.global_settings, data)
720
715
721 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
722 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
723 self.GLOBAL_SVN_SETTINGS, data)
724
725 self._create_or_update_ui(
726 self.global_settings, *http_requests_enabled,
727 value=safe_str(data[http_requests_enabled_key]))
728 self._create_or_update_ui(
729 self.global_settings, *http_server_url,
730 value=data[http_server_url_key])
731
732 def update_global_ssl_setting(self, value):
716 def update_global_ssl_setting(self, value):
733 self._create_or_update_ui(
717 self._create_or_update_ui(
734 self.global_settings, *self.SSL_SETTING, value=value)
718 self.global_settings, *self.SSL_SETTING, value=value)
735
719
736 def update_global_path_setting(self, value):
737 self._create_or_update_ui(
738 self.global_settings, *self.PATH_SETTING, value=value)
739
740 @assert_repo_settings
720 @assert_repo_settings
741 def delete_repo_svn_pattern(self, id_):
721 def delete_repo_svn_pattern(self, id_):
742 ui = self.repo_settings.UiDbModel.get(id_)
722 ui = self.repo_settings.UiDbModel.get(id_)
@@ -811,9 +791,6 b' class VcsSettingsModel(object):'
811 else:
791 else:
812 return self.get_repo_general_settings()
792 return self.get_repo_general_settings()
813
793
814 def get_repos_location(self):
815 return self.global_settings.get_ui_by_key('/').ui_value
816
817 def _filter_ui_settings(self, settings):
794 def _filter_ui_settings(self, settings):
818 filtered_settings = [
795 filtered_settings = [
819 s for s in settings if self._should_keep_setting(s)]
796 s for s in settings if self._should_keep_setting(s)]
@@ -37,7 +37,7 b' from rhodecode.lib.str_utils import safe'
37 from rhodecode.lib.exceptions import (
37 from rhodecode.lib.exceptions import (
38 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
38 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
39 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
40 UserOwnsPullRequestsException, UserOwnsArtifactsException)
40 UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError)
41 from rhodecode.lib.caching_query import FromCache
41 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.model import BaseModel
42 from rhodecode.model import BaseModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
@@ -114,6 +114,7 b' class UserModel(BaseModel):'
114 else:
114 else:
115 user = self.sa.query(User)\
115 user = self.sa.query(User)\
116 .filter(User.username == username)
116 .filter(User.username == username)
117
117 if cache:
118 if cache:
118 name_key = _hash_key(username)
119 name_key = _hash_key(username)
119 user = user.options(
120 user = user.options(
@@ -308,6 +309,10 b' class UserModel(BaseModel):'
308 log.debug('Checking for existing account in RhodeCode '
309 log.debug('Checking for existing account in RhodeCode '
309 'database with user_id `%s` ', updating_user_id)
310 'database with user_id `%s` ', updating_user_id)
310 user = User.get(updating_user_id)
311 user = User.get(updating_user_id)
312 # now also validate if USERNAME belongs to potentially other user
313 maybe_other_user = User.get_by_username(username, case_insensitive=True)
314 if maybe_other_user and maybe_other_user.user_id != updating_user_id:
315 raise DuplicateUpdateUserError(f'different user exists with the {username} username')
311 else:
316 else:
312 log.debug('Checking for existing account in RhodeCode '
317 log.debug('Checking for existing account in RhodeCode '
313 'database with username `%s` ', username)
318 'database with username `%s` ', username)
@@ -761,25 +766,29 b' class UserModel(BaseModel):'
761 'AuthUser: fill data execution based on: '
766 'AuthUser: fill data execution based on: '
762 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
767 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
763 try:
768 try:
769 found_with = ''
764 dbuser = None
770 dbuser = None
765 if user_id:
771 if user_id:
766 dbuser = self.get(user_id)
772 dbuser = self.get(user_id)
773 found_with = 'user_id'
767 elif api_key:
774 elif api_key:
768 dbuser = self.get_by_auth_token(api_key)
775 dbuser = self.get_by_auth_token(api_key)
776 found_with = 'auth_token'
769 elif username:
777 elif username:
770 dbuser = self.get_by_username(username)
778 dbuser = self.get_by_username(username)
779 found_with = 'username'
771
780
772 if not dbuser:
781 if not dbuser:
773 log.warning(
782 log.warning(
774 'Unable to lookup user by id:%s api_key:%s username:%s',
783 'Unable to lookup user by id:%s api_key:%s username:%s, found with: %s',
775 user_id, token_obfuscate(api_key), username)
784 user_id, token_obfuscate(api_key), username, found_with)
776 return False
785 return False
777 if not dbuser.active:
786 if not dbuser.active:
778 log.debug('User `%s:%s` is inactive, skipping fill data',
787 log.debug('User `%s:%s` is inactive, skipping fill data',
779 username, user_id)
788 username, user_id)
780 return False
789 return False
781
790
782 log.debug('AuthUser: filling found user:%s data', dbuser)
791 log.debug('AuthUser: filling found user:%s data, found with: %s', dbuser, found_with)
783
792
784 attrs = {
793 attrs = {
785 'user_id': dbuser.user_id,
794 'user_id': dbuser.user_id,
@@ -64,6 +64,7 b' class ChangePasswordSchema(colander.Sche'
64
64
65 @colander.deferred
65 @colander.deferred
66 def deferred_username_validator(node, kw):
66 def deferred_username_validator(node, kw):
67 old_username = kw.get('username')
67
68
68 def name_validator(node, value):
69 def name_validator(node, value):
69 msg = _(
70 msg = _(
@@ -74,6 +75,11 b' def deferred_username_validator(node, kw'
74 if not re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value):
75 if not re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value):
75 raise colander.Invalid(node, msg)
76 raise colander.Invalid(node, msg)
76
77
78 if value != old_username:
79 existing_user = User.get_by_username(value, case_insensitive=True)
80 if existing_user:
81 raise colander.Invalid(node, 'Username is already taken')
82
77 return name_validator
83 return name_validator
78
84
79
85
@@ -432,7 +432,7 b' def ValidAuth(localizer):'
432
432
433 if not authenticate(username, password, '', HTTP_TYPE,
433 if not authenticate(username, password, '', HTTP_TYPE,
434 skip_missing=True):
434 skip_missing=True):
435 user = User.get_by_username(username)
435 user = User.get_by_username_or_primary_email(username)
436 if user and not user.active:
436 if user and not user.active:
437 log.warning('user %s is disabled', username)
437 log.warning('user %s is disabled', username)
438 msg = M(self, 'disabled_account', state)
438 msg = M(self, 'disabled_account', state)
@@ -91,10 +91,12 b' function registerRCRoutes() {'
91 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
91 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
92 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
92 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
93 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
93 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
94 pyroutes.register('branch_remove', '/%(repo_name)s/branches/%(branch_name)s/remove', ['repo_name', 'branch_name']);
94 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
95 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
95 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
96 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
96 pyroutes.register('channelstream_proxy', '/_channelstream', []);
97 pyroutes.register('channelstream_proxy', '/_channelstream', []);
97 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
98 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
99 pyroutes.register('check_2fa', '/_admin/check_2fa', []);
98 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
100 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
99 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
101 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
100 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
102 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
@@ -214,6 +216,8 b' function registerRCRoutes() {'
214 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
216 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
215 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
217 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
216 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
218 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
219 pyroutes.register('my_account_configure_2fa', '/_admin/my_account/configure_2fa', []);
220 pyroutes.register('my_account_configure_2fa_update', '/_admin/my_account/configure_2fa_update', []);
217 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
221 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
218 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
222 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
219 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
223 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
@@ -230,7 +234,9 b' function registerRCRoutes() {'
230 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
234 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
231 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
235 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
232 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
236 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
237 pyroutes.register('my_account_regenerate_2fa_recovery_codes', '/_admin/my_account/regenerate_recovery_codes', []);
233 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
238 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
239 pyroutes.register('my_account_show_2fa_recovery_codes', '/_admin/my_account/recovery_codes', []);
234 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
240 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
235 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
241 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
236 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
242 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
@@ -243,6 +249,7 b' function registerRCRoutes() {'
243 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
249 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
244 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
250 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
245 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
251 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
252 pyroutes.register('ops_celery_error_test', '/_admin/ops/error-celery', []);
246 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
253 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
247 pyroutes.register('ops_healthcheck', '/_admin/ops/status', []);
254 pyroutes.register('ops_healthcheck', '/_admin/ops/status', []);
248 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
255 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
@@ -379,6 +386,7 b' function registerRCRoutes() {'
379 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
386 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
380 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
387 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
381 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
388 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
389 pyroutes.register('setup_2fa', '/_admin/setup_2fa', []);
382 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
390 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
383 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
391 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
384 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
392 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
@@ -116,8 +116,9 b' def scan_repositories_if_enabled(event):'
116 import_on_startup = settings['startup.import_repos']
116 import_on_startup = settings['startup.import_repos']
117 if vcs_server_enabled and import_on_startup:
117 if vcs_server_enabled and import_on_startup:
118 from rhodecode.model.scm import ScmModel
118 from rhodecode.model.scm import ScmModel
119 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
119 from rhodecode.lib.utils import repo2db_mapper
120 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
120 scm = ScmModel()
121 repositories = scm.repo_scan(scm.repos_path)
121 repo2db_mapper(repositories, remove_obsolete=False)
122 repo2db_mapper(repositories, remove_obsolete=False)
122
123
123
124
@@ -63,7 +63,12 b''
63 %elif node.widget == "password":
63 %elif node.widget == "password":
64 ${h.password(node.name, defaults.get(node.name), class_="large")}
64 ${h.password(node.name, defaults.get(node.name), class_="large")}
65 %elif node.widget == "bool":
65 %elif node.widget == "bool":
66 %if node.name == "global_2fa" and c.rhodecode_edition_id != "EE":
67 <input type="checkbox" disabled/>
68 <%node.description = _('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')%>
69 %else:
66 <div class="checkbox">${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
70 <div class="checkbox" >${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
71 %endif
67 %elif node.widget == "select":
72 %elif node.widget == "select":
68 ${h.select(node.name, defaults.get(node.name), node.validator.choices, class_="select2AuthSetting")}
73 ${h.select(node.name, defaults.get(node.name), node.validator.choices, class_="select2AuthSetting")}
69 %elif node.widget == "select_with_labels":
74 %elif node.widget == "select_with_labels":
@@ -80,7 +85,7 b''
80 <span class="error-message">${errors.get(node.name)}</span>
85 <span class="error-message">${errors.get(node.name)}</span>
81 <br />
86 <br />
82 %endif
87 %endif
83 <p class="help-block pre-formatting">${node.description}</p>
88 <p class="help-block pre-formatting">${node.description | n}</p>
84 </div>
89 </div>
85 </div>
90 </div>
86 %endfor
91 %endfor
@@ -28,6 +28,7 b''
28 <li class="${h.is_active(['profile', 'profile_edit'], c.active)}"><a href="${h.route_path('my_account_profile')}">${_('Profile')}</a></li>
28 <li class="${h.is_active(['profile', 'profile_edit'], c.active)}"><a href="${h.route_path('my_account_profile')}">${_('Profile')}</a></li>
29 <li class="${h.is_active('emails', c.active)}"><a href="${h.route_path('my_account_emails')}">${_('Emails')}</a></li>
29 <li class="${h.is_active('emails', c.active)}"><a href="${h.route_path('my_account_emails')}">${_('Emails')}</a></li>
30 <li class="${h.is_active('password', c.active)}"><a href="${h.route_path('my_account_password')}">${_('Password')}</a></li>
30 <li class="${h.is_active('password', c.active)}"><a href="${h.route_path('my_account_password')}">${_('Password')}</a></li>
31 <li class="${h.is_active('2fa', c.active)}"><a href="${h.route_path('my_account_configure_2fa')}">${_('2FA')}</a></li>
31 <li class="${h.is_active('bookmarks', c.active)}"><a href="${h.route_path('my_account_bookmarks')}">${_('Bookmarks')}</a></li>
32 <li class="${h.is_active('bookmarks', c.active)}"><a href="${h.route_path('my_account_bookmarks')}">${_('Bookmarks')}</a></li>
32 <li class="${h.is_active('auth_tokens', c.active)}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
33 <li class="${h.is_active('auth_tokens', c.active)}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
33 <li class="${h.is_active(['ssh_keys', 'ssh_keys_generate'], c.active)}"><a href="${h.route_path('my_account_ssh_keys')}">${_('SSH Keys')}</a></li>
34 <li class="${h.is_active(['ssh_keys', 'ssh_keys_generate'], c.active)}"><a href="${h.route_path('my_account_ssh_keys')}">${_('SSH Keys')}</a></li>
@@ -83,14 +83,14 b''
83 <table class="rctable">
83 <table class="rctable">
84 ## generate always 10 entries
84 ## generate always 10 entries
85 <input type="hidden" name="__start__" value="bookmarks:sequence"/>
85 <input type="hidden" name="__start__" value="bookmarks:sequence"/>
86 % for item in (c.bookmark_items + [None for i in range(10)])[:10]:
86 % for item in (c.user_bookmark_items + [None for i in range(10)])[:10]:
87 <input type="hidden" name="__start__" value="bookmark:mapping"/>
87 <input type="hidden" name="__start__" value="bookmark:mapping"/>
88 % if item is None:
88 % if item is None:
89 ## empty placehodlder
89 ## empty placehodlder
90 ${form_item()}
90 ${form_item()}
91 % else:
91 % else:
92 ## actual entry
92 ## actual entry
93 ${form_item(position=item.position, title=item.title, redirect_url=item.redirect_url, repo=item.repository, repo_group=item.repository_group)}
93 ${form_item(position=item[0].position, title=item[0].title, redirect_url=item[0].redirect_url, repo=item[1], repo_group=item[2])}
94 % endif
94 % endif
95 <input type="hidden" name="__end__" value="bookmark:mapping"/>
95 <input type="hidden" name="__end__" value="bookmark:mapping"/>
96 % endfor
96 % endfor
@@ -84,6 +84,6 b''
84 <script>
84 <script>
85 $('#check_for_update').click(function(e){
85 $('#check_for_update').click(function(e){
86 $('#update_notice').show();
86 $('#update_notice').show();
87 $('#update_notice').load("${h.route_path('admin_settings_system_update')}");
87 $('#update_notice').load("${h.route_path('admin_settings_system_update', _query={'ver': request.GET.get('ver')})}");
88 })
88 })
89 </script>
89 </script>
@@ -1,25 +1,30 b''
1 ## upgrade block rendered afte on-click check
1 ## upgrade block rendered afte on-click check
2
2
3 <div class="alert ${'alert-warning' if c.should_upgrade else 'alert-success'}">
3 <div class="alert ${'alert-warning' if c.should_upgrade else 'alert-success'}">
4 <p>
4
5 %if c.should_upgrade:
5 %if c.should_upgrade:
6 A <b>new version</b> is available:
6 <span style="font-size: 130%">A <b>new version</b> is available !</span>
7 <br/>
8 <br/>
9
7 %if c.latest_data.get('title'):
10 %if c.latest_data.get('title'):
8 <b>${h.literal(c.latest_data['title'])}</b>
11 RhodeCode <b>${c.latest_ver}</b> - ${h.literal(c.latest_data['title'])}
9 %else:
12 %else:
10 <b>${c.latest_ver}</b>
13 RhodeCode <b>${c.latest_ver}</b>
11 %endif
14 %endif
12 %else:
15 %else:
13 This instance is already running the <b>latest</b> stable version ${c.latest_ver}.
16 Your current version, ${c.cur_ver}, is up-to-date as it is equal to or newer than the latest available version, ${c.latest_ver}.
14 %endif
17 %endif
15 </p>
18
16
19
17 % if c.should_upgrade and c.important_notices:
20 % if c.should_upgrade and c.important_notices:
18 <div>Important notes for this release:</div>
21 <br/>
19 <ul>
22 <br/>
23 <div>Summary:</div>
24 <br/>
20 % for notice in c.important_notices:
25 % for notice in c.important_notices:
21 <li>- ${notice}</li>
26 - ${notice}<br/>
22 % endfor
27 % endfor
23 </ul>
24 % endif
28 % endif
29
25 </div>
30 </div>
@@ -6,8 +6,7 b''
6 suffix='',
6 suffix='',
7 svn_tag_patterns=c.svn_tag_patterns,
7 svn_tag_patterns=c.svn_tag_patterns,
8 svn_branch_patterns=c.svn_branch_patterns,
8 svn_branch_patterns=c.svn_branch_patterns,
9 display_globals=True,
9 display_globals=True
10 allow_repo_location_change=c.visual.allow_repo_location_change
11 )}
10 )}
12 <div class="buttons">
11 <div class="buttons">
13 ${h.submit('save',_('Save settings'),class_="btn")}
12 ${h.submit('save',_('Save settings'),class_="btn")}
@@ -50,16 +49,5 b''
50 unlockpath();
49 unlockpath();
51 }
50 }
52
51
53 /* On click handler for the `Generate Apache Config` button. It sends a
54 POST request to trigger the (re)generation of the mod_dav_svn config. */
55 $('#vcs_svn_generate_cfg').on('click', function(event) {
56 event.preventDefault();
57 var url = "${h.route_path('admin_settings_vcs_svn_generate_cfg')}";
58 var jqxhr = $.post(url, {'csrf_token': CSRF_TOKEN});
59 jqxhr.done(function(data) {
60 $.Topic('/notifications').publish(data);
61 });
62 });
63
64 });
52 });
65 </script>
53 </script>
@@ -651,26 +651,26 b''
651 % endif
651 % endif
652 % for item in c.bookmark_items:
652 % for item in c.bookmark_items:
653 <li>
653 <li>
654 % if item.repository:
654 % if item.repo_id:
655 <div>
655 <div>
656 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
656 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
657 <code>${item.position}</code>
657 <code>${item.position}</code>
658 % if item.repository.repo_type == 'hg':
658 % if item.repo_type == 'hg':
659 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
659 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
660 % elif item.repository.repo_type == 'git':
660 % elif item.repo_type == 'git':
661 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
661 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
662 % elif item.repository.repo_type == 'svn':
662 % elif item.repo_type == 'svn':
663 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
663 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
664 % endif
664 % endif
665 ${(item.title or h.shorter(item.repository.repo_name, 30))}
665 ${(item.title or h.shorter(item.repo_name, 30))}
666 </a>
666 </a>
667 </div>
667 </div>
668 % elif item.repository_group:
668 % elif item.group_id:
669 <div>
669 <div>
670 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
670 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
671 <code>${item.position}</code>
671 <code>${item.position}</code>
672 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
672 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
673 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
673 ${(item.title or h.shorter(item.group_name, 30))}
674 </a>
674 </a>
675 </div>
675 </div>
676 % else:
676 % else:
@@ -17,7 +17,7 b' examples = ['
17
17
18 (
18 (
19 'Tickets with #123 (Redmine etc)',
19 'Tickets with #123 (Redmine etc)',
20 '(?<![a-zA-Z0-9_/]{1,10}-?)(#)(?P<issue_id>\d+)',
20 '(?<![a-zA-Z0-9_/]{1,10}-?)(#)(?P<issue_id>[0-9]+)',
21 'https://myissueserver.com/${repo}/issue/${issue_id}',
21 'https://myissueserver.com/${repo}/issue/${issue_id}',
22 ''
22 ''
23 ),
23 ),
@@ -60,7 +60,7 b' examples = ['
60
60
61 (
61 (
62 'Pivotal Tracker',
62 'Pivotal Tracker',
63 '(?:pivot-)(?P<project_id>\d+)-(?P<story>\d+)',
63 '(?:pivot-)(?P<project_id>\d+)-(?P<story>[0-9]+)',
64 'https://www.pivotaltracker.com/s/projects/${project_id}/stories/${story}',
64 'https://www.pivotaltracker.com/s/projects/${project_id}/stories/${story}',
65 'PIV-',
65 'PIV-',
66 ),
66 ),
@@ -3,7 +3,7 b''
3 ## <%namespace name="vcss" file="/base/vcssettings.mako"/>
3 ## <%namespace name="vcss" file="/base/vcssettings.mako"/>
4 ## ${vcss.vcs_settings_fields()}
4 ## ${vcss.vcs_settings_fields()}
5
5
6 <%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)">
6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, **kwargs)">
7 % if display_globals:
7 % if display_globals:
8 <div class="panel panel-default">
8 <div class="panel panel-default">
9 <div class="panel-heading" id="general">
9 <div class="panel-heading" id="general">
@@ -23,34 +23,6 b''
23 </div>
23 </div>
24 % endif
24 % endif
25
25
26 % if display_globals:
27 <div class="panel panel-default">
28 <div class="panel-heading" id="vcs-storage-options">
29 <h3 class="panel-title">${_('Main Storage Location')}<a class="permalink" href="#vcs-storage-options"> ¶</a></h3>
30 </div>
31 <div class="panel-body">
32 <div class="field">
33 <div class="inputx locked_input">
34 %if allow_repo_location_change:
35 ${h.text('paths_root_path',size=59,readonly="readonly", class_="disabled")}
36 <span id="path_unlock" class="tooltip"
37 title="${h.tooltip(_('Click to unlock. You must restart RhodeCode in order to make this setting take effect.'))}">
38 <div class="btn btn-default lock_input_button"><i id="path_unlock_icon" class="icon-lock"></i></div>
39 </span>
40 %else:
41 ${_('Repository location change is disabled. You can enable this by changing the `allow_repo_location_change` inside .ini file.')}
42 ## form still requires this but we cannot internally change it anyway
43 ${h.hidden('paths_root_path',size=30,readonly="readonly", class_="disabled")}
44 %endif
45 </div>
46 </div>
47 <div class="label">
48 <span class="help-block">${_('Filesystem location where repositories should be stored. After changing this value a restart and rescan of the repository folder are required.')}</span>
49 </div>
50 </div>
51 </div>
52 % endif
53
54 % if display_globals or repo_type in ['git', 'hg']:
26 % if display_globals or repo_type in ['git', 'hg']:
55 <div class="panel panel-default">
27 <div class="panel panel-default">
56 <div class="panel-heading" id="vcs-hooks-options">
28 <div class="panel-heading" id="vcs-hooks-options">
@@ -170,48 +142,31 b''
170 </div>
142 </div>
171 % endif
143 % endif
172
144
173
174 % if display_globals:
175 <div class="panel panel-default">
176 <div class="panel-heading" id="vcs-global-svn-options">
177 <h3 class="panel-title">${_('Global Subversion Settings')}<a class="permalink" href="#vcs-global-svn-options"> ¶</a></h3>
178 </div>
179 <div class="panel-body">
180 <div class="field">
181 <div class="checkbox">
182 ${h.checkbox('vcs_svn_proxy_http_requests_enabled' + suffix, 'True', **kwargs)}
183 <label for="vcs_svn_proxy_http_requests_enabled${suffix}">${_('Proxy subversion HTTP requests')}</label>
184 </div>
185 <div class="label">
186 <span class="help-block">
187 ${_('Subversion HTTP Support. Enables communication with SVN over HTTP protocol.')}
188 <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('SVN Protocol setup Documentation')}</a>.
189 </span>
190 </div>
191 </div>
192 <div class="field">
193 <div class="label">
194 <label for="vcs_svn_proxy_http_server_url">${_('Subversion HTTP Server URL')}</label><br/>
195 </div>
196 <div class="input">
197 ${h.text('vcs_svn_proxy_http_server_url',size=59)}
198 % if c.svn_proxy_generate_config:
199 <span class="buttons">
200 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Generate Apache Config')}</button>
201 </span>
202 % endif
203 </div>
204 </div>
205 </div>
206 </div>
207 % endif
208
209 % if display_globals or repo_type in ['svn']:
145 % if display_globals or repo_type in ['svn']:
210 <div class="panel panel-default">
146 <div class="panel panel-default">
211 <div class="panel-heading" id="vcs-svn-options">
147 <div class="panel-heading" id="vcs-svn-options">
212 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"> ¶</a></h3>
148 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"> ¶</a></h3>
213 </div>
149 </div>
214 <div class="panel-body">
150 <div class="panel-body">
151 % if display_globals:
152 <div class="field">
153 <div class="content" >
154 <label>${_('mod_dav config')}</label><br/>
155 <code>path: ${c.svn_config_path}</code>
156 </div>
157 <br/>
158
159 <div>
160
161 % if c.svn_generate_config:
162 <span class="buttons">
163 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Re-generate Apache Config')}</button>
164 </span>
165 % endif
166 </div>
167 </div>
168 % endif
169
215 <div class="field">
170 <div class="field">
216 <div class="content" >
171 <div class="content" >
217 <label>${_('Repository patterns')}</label><br/>
172 <label>${_('Repository patterns')}</label><br/>
@@ -345,13 +300,12 b''
345 </div>
300 </div>
346 % endif
301 % endif
347
302
348 ## DISABLED FOR GIT FOR NOW as the rebase/close is not supported yet
303 % if display_globals or repo_type in ['git']:
349 ## % if display_globals or repo_type in ['git']:
304 <div class="panel panel-default">
350 ## <div class="panel panel-default">
305 <div class="panel-heading" id="vcs-pull-requests-options">
351 ## <div class="panel-heading" id="vcs-pull-requests-options">
306 <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"> ¶</a></h3>
352 ## <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"> ¶</a></h3>
307 </div>
353 ## </div>
308 <div class="panel-body">
354 ## <div class="panel-body">
355 ## <div class="checkbox">
309 ## <div class="checkbox">
356 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
310 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
357 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
311 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
@@ -359,17 +313,33 b''
359 ## <div class="label">
313 ## <div class="label">
360 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
314 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
361 ## </div>
315 ## </div>
362 ##
316
363 ## <div class="checkbox">
317 <div class="checkbox">
364 ## ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
318 ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
365 ## <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
319 <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
366 ## </div>
320 </div>
367 ## <div class="label">
321 <div class="label">
368 ## <span class="help-block">${_('Delete branch after merging it into destination branch. No effect when rebase strategy is use.')}</span>
322 <span class="help-block">${_('Delete branch after merging it into destination branch.')}</span>
369 ## </div>
323 </div>
370 ## </div>
324 </div>
371 ## </div>
325 </div>
372 ## % endif
326 % endif
327
328 <script type="text/javascript">
373
329
330 $(document).ready(function() {
331 /* On click handler for the `Generate Apache Config` button. It sends a
332 POST request to trigger the (re)generation of the mod_dav_svn config. */
333 $('#vcs_svn_generate_cfg').on('click', function(event) {
334 event.preventDefault();
335 var url = "${h.route_path('admin_settings_vcs_svn_generate_cfg')}";
336 var jqxhr = $.post(url, {'csrf_token': CSRF_TOKEN});
337 jqxhr.done(function(data) {
338 $.Topic('/notifications').publish(data);
339 });
340 });
341 });
374
342
343 </script>
375 </%def>
344 </%def>
345
@@ -62,13 +62,8 b''
62 };
62 };
63
63
64 var branches_data = ${c.data|n};
64 var branches_data = ${c.data|n};
65 // object list
65 var repo_type = "${c.rhodecode_db_repo.repo_type}";
66 $('#obj_list_table').DataTable({
66 var columns = [
67 data: branches_data,
68 dom: 'rtp',
69 pageLength: ${c.visual.dashboard_items},
70 order: [[ 0, "asc" ]],
71 columns: [
72 { data: {"_": "name",
67 { data: {"_": "name",
73 "sort": "name_raw"}, title: "${_('Name')}", className: "td-tags" },
68 "sort": "name_raw"}, title: "${_('Name')}", className: "td-tags" },
74 { data: {"_": "date",
69 { data: {"_": "date",
@@ -80,7 +75,22 b''
80 "type": Number}, title: "${_('Commit')}", className: "td-hash" },
75 "type": Number}, title: "${_('Commit')}", className: "td-hash" },
81 { data: {"_": "compare",
76 { data: {"_": "compare",
82 "sort": "compare"}, title: "${_('Compare')}", className: "td-compare" }
77 "sort": "compare"}, title: "${_('Compare')}", className: "td-compare" }
83 ],
78 ];
79 if (repo_type !== 'svn') {
80 columns.push({
81 data: { "_": "action", "sort": "action" },
82 title: `${_('Action')}`,
83 className: "td-action",
84 orderable: false
85 });
86 }
87
88 $('#obj_list_table').DataTable({
89 data: branches_data,
90 dom: 'rtp',
91 pageLength: ${c.visual.dashboard_items},
92 order: [[ 0, "asc" ]],
93 columns: columns,
84 language: {
94 language: {
85 paginate: DEFAULT_GRID_PAGINATION,
95 paginate: DEFAULT_GRID_PAGINATION,
86 emptyTable: _gettext("No branches available yet.")
96 emptyTable: _gettext("No branches available yet.")
@@ -277,6 +277,30 b''
277 </div>
277 </div>
278 </%def>
278 </%def>
279
279
280 <%def name="branch_actions_git(branch_name, repo_name, **kwargs)">
281 <div class="grid_delete">
282 ${h.secure_form(h.route_path('branch_remove', repo_name=repo_name, branch_name=branch_name), request=request)}
283 <input class="btn btn-link btn-danger" id="remove_branch_${branch_name}" name="remove_branch_${branch_name}"
284 onclick="submitConfirm(event, this, _gettext('Confirm to delete this branch'), _gettext('Delete'), '${branch_name}')"
285 type="submit" value="Delete"
286 >
287 ${h.end_form()}
288 </div>
289 </%def>
290
291 <%def name="branch_actions_hg(branch_name, repo_name, **kwargs)">
292 <div class="grid_delete">
293 %if not kwargs['closed']:
294 ${h.secure_form(h.route_path('branch_remove', repo_name=repo_name, branch_name=branch_name), request=request)}
295 <input class="btn btn-link btn-danger" id="remove_branch_${branch_name}" name="remove_branch_${branch_name}"
296 onclick="submitConfirm(event, this, _gettext('Confirm to close this branch'), _gettext('Close'), '${branch_name}')"
297 type="submit" value="Close"
298 >
299 ${h.end_form()}
300 %endif
301 </div>
302 </%def>
303
280 <%def name="user_group_actions(user_group_id, user_group_name)">
304 <%def name="user_group_actions(user_group_id, user_group_name)">
281 <div class="grid_edit">
305 <div class="grid_edit">
282 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
306 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
@@ -269,6 +269,18 b' They are permanent until deleted, or con'
269 <pre><%= submodule_url %></pre>
269 <pre><%= submodule_url %></pre>
270 </script>
270 </script>
271
271
272 <script id="ejs_recoveryCodes" type="text/template" class="ejsTemplate">
273 <code>
274 <ol>
275 <% for (var i = 0; i < recoveryCodes.length; i++) { %>
276 <% var code = recoveryCodes[i] %>
277 <li><%= code %></li>
278 <% } %>
279 </ol>
280 </code>
281 <i class="icon-clipboard clipboard-action" data-clipboard-text="<%= recoveryCodes %>" >Copy All</i>
282 </script>
283
272 ##// END OF EJS Templates
284 ##// END OF EJS Templates
273 </div>
285 </div>
274
286
@@ -35,12 +35,12 b''
35 <%block name="above_login_button" />
35 <%block name="above_login_button" />
36 <!-- login -->
36 <!-- login -->
37 <div class="sign-in-title">
37 <div class="sign-in-title">
38 <h1>${_('Sign In using username/password')}</h1>
38 <h1>${_('Sign In using credentials')}</h1>
39 </div>
39 </div>
40 <div class="inner form">
40 <div class="inner form">
41 ${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
41 ${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
42
42
43 <label for="username">${_('Username')}:</label>
43 <label for="username">${_('Username or email address')}:</label>
44 ${h.text('username', class_='focus', value=defaults.get('username'))}
44 ${h.text('username', class_='focus', value=defaults.get('username'))}
45 %if 'username' in errors:
45 %if 'username' in errors:
46 <span class="error-message">${errors.get('username')}</span>
46 <span class="error-message">${errors.get('username')}</span>
@@ -27,6 +27,7 b' import urllib.parse'
27
27
28 import pytest
28 import pytest
29
29
30 import rhodecode
30 from rhodecode.model.db import User
31 from rhodecode.model.db import User
31 from rhodecode.lib import auth
32 from rhodecode.lib import auth
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
@@ -54,7 +55,6 b' log = logging.getLogger(__name__)'
54 # SOME GLOBALS FOR TESTS
55 # SOME GLOBALS FOR TESTS
55 TEST_DIR = tempfile.gettempdir()
56 TEST_DIR = tempfile.gettempdir()
56
57
57 TESTS_TMP_PATH = jn(TEST_DIR, 'rc_test_{}'.format(next(tempfile._RandomNameSequence())))
58 TEST_USER_ADMIN_LOGIN = 'test_admin'
58 TEST_USER_ADMIN_LOGIN = 'test_admin'
59 TEST_USER_ADMIN_PASS = 'test12'
59 TEST_USER_ADMIN_PASS = 'test12'
60 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
60 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
@@ -81,6 +81,8 b" GIT_FORK = 'vcs_test_git_fork'"
81 SCM_TESTS = ['hg', 'git']
81 SCM_TESTS = ['hg', 'git']
82 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
82 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
83
83
84 TESTS_TMP_PATH = tempfile.mkdtemp(prefix='rc_test_', dir=TEST_DIR)
85
84 TEST_GIT_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
86 TEST_GIT_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
85 TEST_GIT_REPO_CLONE = jn(TESTS_TMP_PATH, f'vcsgitclone{uniq_suffix}')
87 TEST_GIT_REPO_CLONE = jn(TESTS_TMP_PATH, f'vcsgitclone{uniq_suffix}')
86 TEST_GIT_REPO_PULL = jn(TESTS_TMP_PATH, f'vcsgitpull{uniq_suffix}')
88 TEST_GIT_REPO_PULL = jn(TESTS_TMP_PATH, f'vcsgitpull{uniq_suffix}')
@@ -111,7 +113,7 b' def get_new_dir(title):'
111 hex_str = sha1_safe(f'{os.getpid()} {time.time()}')
113 hex_str = sha1_safe(f'{os.getpid()} {time.time()}')
112 name_parts.append(hex_str)
114 name_parts.append(hex_str)
113 name = '-'.join(name_parts)
115 name = '-'.join(name_parts)
114 path = os.path.join(TEST_DIR, name)
116 path = jn(TEST_DIR, name)
115 return get_normalized_path(path)
117 return get_normalized_path(path)
116
118
117
119
@@ -44,7 +44,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
44
44
45 @hybrid_property
45 @hybrid_property
46 def name(self):
46 def name(self):
47 return u"external_test"
47 return "external_test"
48
48
49 def settings(self):
49 def settings(self):
50 settings = [
50 settings = [
@@ -82,10 +82,11 b' def pytest_addoption(parser):'
82 parser.addoption(
82 parser.addoption(
83 '--test-loglevel', dest='test_loglevel',
83 '--test-loglevel', dest='test_loglevel',
84 help="Set default Logging level for tests, critical(default), error, warn , info, debug")
84 help="Set default Logging level for tests, critical(default), error, warn , info, debug")
85 group = parser.getgroup('pylons')
85
86 group = parser.getgroup('pyramid')
86 group.addoption(
87 group.addoption(
87 '--with-pylons', dest='pyramid_config',
88 '--pyramid-config', dest='pyramid_config',
88 help="Set up a Pylons environment with the specified config file.")
89 help="Set up a pyramid with the specified ini config file.")
89 group.addoption(
90 group.addoption(
90 '--ini-config-override', action='store', type=_parse_json,
91 '--ini-config-override', action='store', type=_parse_json,
91 default=None, dest='pyramid_config_override', help=(
92 default=None, dest='pyramid_config_override', help=(
@@ -86,10 +86,10 b' class DBBackend(object):'
86 _store = os.path.dirname(os.path.abspath(__file__))
86 _store = os.path.dirname(os.path.abspath(__file__))
87 _type = None
87 _type = None
88 _base_ini_config = [{'app:main': {'vcs.start_server': 'false',
88 _base_ini_config = [{'app:main': {'vcs.start_server': 'false',
89 'startup.import_repos': 'false',
89 'startup.import_repos': 'false'}}]
90 'is_test': 'False'}}]
91 _db_url = [{'app:main': {'sqlalchemy.db1.url': ''}}]
90 _db_url = [{'app:main': {'sqlalchemy.db1.url': ''}}]
92 _base_db_name = 'rhodecode_test_db_backend'
91 _base_db_name = 'rhodecode_test_db_backend'
92 std_env = {'RC_TEST': '0'}
93
93
94 def __init__(
94 def __init__(
95 self, config_file, db_name=None, basetemp=None,
95 self, config_file, db_name=None, basetemp=None,
@@ -135,13 +135,15 b' class DBBackend(object):'
135 """
135 """
136
136
137 command = cmd + ' ' + ' '.join(args)
137 command = cmd + ' ' + ' '.join(args)
138 sys.stdout.write(command)
138 sys.stdout.write(f'CMD: {command}')
139
139
140 # Tell Python to use UTF-8 encoding out stdout
140 # Tell Python to use UTF-8 encoding out stdout
141 _env = os.environ.copy()
141 _env = os.environ.copy()
142 _env['PYTHONIOENCODING'] = 'UTF-8'
142 _env['PYTHONIOENCODING'] = 'UTF-8'
143 _env.update(self.std_env)
143 if env:
144 if env:
144 _env.update(env)
145 _env.update(env)
146
145 self.p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, env=_env)
147 self.p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, env=_env)
146 self.stdout, self.stderr = self.p.communicate()
148 self.stdout, self.stderr = self.p.communicate()
147 stdout_str = safe_str(self.stdout)
149 stdout_str = safe_str(self.stdout)
@@ -19,8 +19,9 b''
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.lib.pyramid_utils import get_app_config
22 from rhodecode.lib.config_utils import get_app_config
23 from rhodecode.tests.fixture import TestINI
23 from rhodecode.tests.fixture import TestINI
24 from rhodecode.tests import TESTS_TMP_PATH
24 from rhodecode.tests.server_utils import RcVCSServer
25 from rhodecode.tests.server_utils import RcVCSServer
25
26
26
27
@@ -57,7 +58,7 b' def vcsserver_factory(tmpdir_factory):'
57 """
58 """
58
59
59 def factory(request, overrides=(), vcsserver_port=None,
60 def factory(request, overrides=(), vcsserver_port=None,
60 log_file=None, workers='2'):
61 log_file=None, workers='3'):
61
62
62 if vcsserver_port is None:
63 if vcsserver_port is None:
63 vcsserver_port = get_available_port()
64 vcsserver_port = get_available_port()
@@ -99,7 +100,7 b' def ini_config(request, tmpdir_factory, '
99 overrides = [
100 overrides = [
100 {'server:main': {'port': rcserver_port}},
101 {'server:main': {'port': rcserver_port}},
101 {'app:main': {
102 {'app:main': {
102 'cache_dir': '%(here)s/rc_data',
103 'cache_dir': '%(here)s/rc-tests/rc_data',
103 'vcs.server': f'localhost:{vcsserver_port}',
104 'vcs.server': f'localhost:{vcsserver_port}',
104 # johbo: We will always start the VCSServer on our own based on the
105 # johbo: We will always start the VCSServer on our own based on the
105 # fixtures of the test cases. For the test run it must always be
106 # fixtures of the test cases. For the test run it must always be
@@ -108,8 +109,11 b' def ini_config(request, tmpdir_factory, '
108
109
109 'vcs.server.protocol': 'http',
110 'vcs.server.protocol': 'http',
110 'vcs.scm_app_implementation': 'http',
111 'vcs.scm_app_implementation': 'http',
112 'vcs.svn.proxy.enabled': 'true',
111 'vcs.hooks.protocol': 'http',
113 'vcs.hooks.protocol': 'http',
112 'vcs.hooks.host': '*',
114 'vcs.hooks.host': '*',
115 'repo_store.path': TESTS_TMP_PATH,
116 'app.service_api.token': 'service_secret_token',
113 }},
117 }},
114
118
115 {'handler_console': {
119 {'handler_console': {
@@ -157,7 +161,7 b' def vcsserver_port(request):'
157
161
158
162
159 @pytest.fixture(scope='session')
163 @pytest.fixture(scope='session')
160 def available_port_factory():
164 def available_port_factory() -> get_available_port:
161 """
165 """
162 Returns a callable which returns free port numbers.
166 Returns a callable which returns free port numbers.
163 """
167 """
@@ -174,7 +174,7 b' def http_environ():'
174
174
175 @pytest.fixture(scope='session')
175 @pytest.fixture(scope='session')
176 def baseapp(ini_config, vcsserver, http_environ_session):
176 def baseapp(ini_config, vcsserver, http_environ_session):
177 from rhodecode.lib.pyramid_utils import get_app_config
177 from rhodecode.lib.config_utils import get_app_config
178 from rhodecode.config.middleware import make_pyramid_app
178 from rhodecode.config.middleware import make_pyramid_app
179
179
180 log.info("Using the RhodeCode configuration:{}".format(ini_config))
180 log.info("Using the RhodeCode configuration:{}".format(ini_config))
@@ -29,7 +29,7 b' from rhodecode.model import db'
29 class RcTestAuthPlugin(RhodeCodeAuthPluginBase):
29 class RcTestAuthPlugin(RhodeCodeAuthPluginBase):
30
30
31 def name(self):
31 def name(self):
32 return u'stub_auth'
32 return 'stub_auth'
33
33
34
34
35 def test_authenticate_returns_from_auth(stub_auth_data):
35 def test_authenticate_returns_from_auth(stub_auth_data):
@@ -23,16 +23,16 b' import pytest'
23 from unittest.mock import patch, Mock, MagicMock
23 from unittest.mock import patch, Mock, MagicMock
24
24
25 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
25 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
26 from rhodecode.lib.utils import get_rhodecode_base_path
26 from rhodecode.lib.utils import get_rhodecode_repo_store_path
27 from rhodecode.tests import SVN_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS
27 from rhodecode.tests import SVN_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS
28
28
29
29
30 class TestSimpleSvn(object):
30 class TestSimpleSvn(object):
31 @pytest.fixture(autouse=True)
31 @pytest.fixture(autouse=True)
32 def simple_svn(self, baseapp, request_stub):
32 def simple_svn(self, baseapp, request_stub):
33 base_path = get_rhodecode_base_path()
33 base_path = get_rhodecode_repo_store_path()
34 self.app = SimpleSvn(
34 self.app = SimpleSvn(
35 config={'auth_ret_code': '', 'base_path': base_path},
35 config={'auth_ret_code': '', 'repo_store.path': base_path},
36 registry=request_stub.registry)
36 registry=request_stub.registry)
37
37
38 def test_get_config(self):
38 def test_get_config(self):
@@ -126,7 +126,7 b' class TestSimpleSvnApp(object):'
126 def setup_method(self, method):
126 def setup_method(self, method):
127 # note(marcink): this is hostname from docker compose used for testing...
127 # note(marcink): this is hostname from docker compose used for testing...
128 self.host = 'http://svn:8090'
128 self.host = 'http://svn:8090'
129 base_path = get_rhodecode_base_path()
129 base_path = get_rhodecode_repo_store_path()
130 self.app = SimpleSvnApp(
130 self.app = SimpleSvnApp(
131 config={'subversion_http_server_url': self.host,
131 config={'subversion_http_server_url': self.host,
132 'base_path': base_path})
132 'base_path': base_path})
@@ -25,17 +25,20 b' import msgpack'
25 import pytest
25 import pytest
26 import tempfile
26 import tempfile
27
27
28 from rhodecode.lib import hooks_daemon
28 from rhodecode.lib.hook_daemon import http_hooks_deamon
29 from rhodecode.lib.hook_daemon import celery_hooks_deamon
30 from rhodecode.lib.hook_daemon import hook_module
31 from rhodecode.lib.hook_daemon import base as hook_base
29 from rhodecode.lib.str_utils import safe_bytes
32 from rhodecode.lib.str_utils import safe_bytes
30 from rhodecode.tests.utils import assert_message_in_log
33 from rhodecode.tests.utils import assert_message_in_log
31 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
32
35
33 test_proto = hooks_daemon.HooksHttpHandler.MSGPACK_HOOKS_PROTO
36 test_proto = http_hooks_deamon.HooksHttpHandler.MSGPACK_HOOKS_PROTO
34
37
35
38
36 class TestHooks(object):
39 class TestHooks(object):
37 def test_hooks_can_be_used_as_a_context_processor(self):
40 def test_hooks_can_be_used_as_a_context_processor(self):
38 hooks = hooks_daemon.Hooks()
41 hooks = hook_module.Hooks()
39 with hooks as return_value:
42 with hooks as return_value:
40 pass
43 pass
41 assert hooks == return_value
44 assert hooks == return_value
@@ -52,10 +55,10 b' class TestHooksHttpHandler(object):'
52 }
55 }
53 request = self._generate_post_request(data)
56 request = self._generate_post_request(data)
54 hooks_patcher = mock.patch.object(
57 hooks_patcher = mock.patch.object(
55 hooks_daemon.Hooks, data['method'], create=True, return_value=1)
58 hook_module.Hooks, data['method'], create=True, return_value=1)
56
59
57 with hooks_patcher as hooks_mock:
60 with hooks_patcher as hooks_mock:
58 handler = hooks_daemon.HooksHttpHandler
61 handler = http_hooks_deamon.HooksHttpHandler
59 handler.DEFAULT_HOOKS_PROTO = test_proto
62 handler.DEFAULT_HOOKS_PROTO = test_proto
60 handler.wbufsize = 10240
63 handler.wbufsize = 10240
61 MockServer(handler, request)
64 MockServer(handler, request)
@@ -73,21 +76,21 b' class TestHooksHttpHandler(object):'
73
76
74 # patching our _read to return test method and proto used
77 # patching our _read to return test method and proto used
75 read_patcher = mock.patch.object(
78 read_patcher = mock.patch.object(
76 hooks_daemon.HooksHttpHandler, '_read_request',
79 http_hooks_deamon.HooksHttpHandler, '_read_request',
77 return_value=(test_proto, rpc_method, extras))
80 return_value=(test_proto, rpc_method, extras))
78
81
79 # patch Hooks instance to return hook_result data on 'test' call
82 # patch Hooks instance to return hook_result data on 'test' call
80 hooks_patcher = mock.patch.object(
83 hooks_patcher = mock.patch.object(
81 hooks_daemon.Hooks, rpc_method, create=True,
84 hook_module.Hooks, rpc_method, create=True,
82 return_value=hook_result)
85 return_value=hook_result)
83
86
84 with read_patcher, hooks_patcher:
87 with read_patcher, hooks_patcher:
85 handler = hooks_daemon.HooksHttpHandler
88 handler = http_hooks_deamon.HooksHttpHandler
86 handler.DEFAULT_HOOKS_PROTO = test_proto
89 handler.DEFAULT_HOOKS_PROTO = test_proto
87 handler.wbufsize = 10240
90 handler.wbufsize = 10240
88 server = MockServer(handler, request)
91 server = MockServer(handler, request)
89
92
90 expected_result = hooks_daemon.HooksHttpHandler.serialize_data(hook_result)
93 expected_result = http_hooks_deamon.HooksHttpHandler.serialize_data(hook_result)
91
94
92 server.request.output_stream.seek(0)
95 server.request.output_stream.seek(0)
93 assert server.request.output_stream.readlines()[-1] == expected_result
96 assert server.request.output_stream.readlines()[-1] == expected_result
@@ -97,15 +100,15 b' class TestHooksHttpHandler(object):'
97 rpc_method = 'test'
100 rpc_method = 'test'
98
101
99 read_patcher = mock.patch.object(
102 read_patcher = mock.patch.object(
100 hooks_daemon.HooksHttpHandler, '_read_request',
103 http_hooks_deamon.HooksHttpHandler, '_read_request',
101 return_value=(test_proto, rpc_method, {}))
104 return_value=(test_proto, rpc_method, {}))
102
105
103 hooks_patcher = mock.patch.object(
106 hooks_patcher = mock.patch.object(
104 hooks_daemon.Hooks, rpc_method, create=True,
107 hook_module.Hooks, rpc_method, create=True,
105 side_effect=Exception('Test exception'))
108 side_effect=Exception('Test exception'))
106
109
107 with read_patcher, hooks_patcher:
110 with read_patcher, hooks_patcher:
108 handler = hooks_daemon.HooksHttpHandler
111 handler = http_hooks_deamon.HooksHttpHandler
109 handler.DEFAULT_HOOKS_PROTO = test_proto
112 handler.DEFAULT_HOOKS_PROTO = test_proto
110 handler.wbufsize = 10240
113 handler.wbufsize = 10240
111 server = MockServer(handler, request)
114 server = MockServer(handler, request)
@@ -113,7 +116,7 b' class TestHooksHttpHandler(object):'
113 server.request.output_stream.seek(0)
116 server.request.output_stream.seek(0)
114 data = server.request.output_stream.readlines()
117 data = server.request.output_stream.readlines()
115 msgpack_data = b''.join(data[5:])
118 msgpack_data = b''.join(data[5:])
116 org_exc = hooks_daemon.HooksHttpHandler.deserialize_data(msgpack_data)
119 org_exc = http_hooks_deamon.HooksHttpHandler.deserialize_data(msgpack_data)
117 expected_result = {
120 expected_result = {
118 'exception': 'Exception',
121 'exception': 'Exception',
119 'exception_traceback': org_exc['exception_traceback'],
122 'exception_traceback': org_exc['exception_traceback'],
@@ -123,8 +126,7 b' class TestHooksHttpHandler(object):'
123
126
124 def test_log_message_writes_to_debug_log(self, caplog):
127 def test_log_message_writes_to_debug_log(self, caplog):
125 ip_port = ('0.0.0.0', 8888)
128 ip_port = ('0.0.0.0', 8888)
126 handler = hooks_daemon.HooksHttpHandler(
129 handler = http_hooks_deamon.HooksHttpHandler(MockRequest('POST /'), ip_port, mock.Mock())
127 MockRequest('POST /'), ip_port, mock.Mock())
128 fake_date = '1/Nov/2015 00:00:00'
130 fake_date = '1/Nov/2015 00:00:00'
129 date_patcher = mock.patch.object(
131 date_patcher = mock.patch.object(
130 handler, 'log_date_time_string', return_value=fake_date)
132 handler, 'log_date_time_string', return_value=fake_date)
@@ -136,10 +138,10 b' class TestHooksHttpHandler(object):'
136
138
137 assert_message_in_log(
139 assert_message_in_log(
138 caplog.records, expected_message,
140 caplog.records, expected_message,
139 levelno=logging.DEBUG, module='hooks_daemon')
141 levelno=logging.DEBUG, module='http_hooks_deamon')
140
142
141 def _generate_post_request(self, data, proto=test_proto):
143 def _generate_post_request(self, data, proto=test_proto):
142 if proto == hooks_daemon.HooksHttpHandler.MSGPACK_HOOKS_PROTO:
144 if proto == http_hooks_deamon.HooksHttpHandler.MSGPACK_HOOKS_PROTO:
143 payload = msgpack.packb(data)
145 payload = msgpack.packb(data)
144 else:
146 else:
145 payload = json.dumps(data)
147 payload = json.dumps(data)
@@ -151,18 +153,18 b' class TestHooksHttpHandler(object):'
151 class ThreadedHookCallbackDaemon(object):
153 class ThreadedHookCallbackDaemon(object):
152 def test_constructor_calls_prepare(self):
154 def test_constructor_calls_prepare(self):
153 prepare_daemon_patcher = mock.patch.object(
155 prepare_daemon_patcher = mock.patch.object(
154 hooks_daemon.ThreadedHookCallbackDaemon, '_prepare')
156 http_hooks_deamon.ThreadedHookCallbackDaemon, '_prepare')
155 with prepare_daemon_patcher as prepare_daemon_mock:
157 with prepare_daemon_patcher as prepare_daemon_mock:
156 hooks_daemon.ThreadedHookCallbackDaemon()
158 http_hooks_deamon.ThreadedHookCallbackDaemon()
157 prepare_daemon_mock.assert_called_once_with()
159 prepare_daemon_mock.assert_called_once_with()
158
160
159 def test_run_is_called_on_context_start(self):
161 def test_run_is_called_on_context_start(self):
160 patchers = mock.patch.multiple(
162 patchers = mock.patch.multiple(
161 hooks_daemon.ThreadedHookCallbackDaemon,
163 http_hooks_deamon.ThreadedHookCallbackDaemon,
162 _run=mock.DEFAULT, _prepare=mock.DEFAULT, __exit__=mock.DEFAULT)
164 _run=mock.DEFAULT, _prepare=mock.DEFAULT, __exit__=mock.DEFAULT)
163
165
164 with patchers as mocks:
166 with patchers as mocks:
165 daemon = hooks_daemon.ThreadedHookCallbackDaemon()
167 daemon = http_hooks_deamon.ThreadedHookCallbackDaemon()
166 with daemon as daemon_context:
168 with daemon as daemon_context:
167 pass
169 pass
168 mocks['_run'].assert_called_once_with()
170 mocks['_run'].assert_called_once_with()
@@ -170,11 +172,11 b' class ThreadedHookCallbackDaemon(object)'
170
172
171 def test_stop_is_called_on_context_exit(self):
173 def test_stop_is_called_on_context_exit(self):
172 patchers = mock.patch.multiple(
174 patchers = mock.patch.multiple(
173 hooks_daemon.ThreadedHookCallbackDaemon,
175 http_hooks_deamon.ThreadedHookCallbackDaemon,
174 _run=mock.DEFAULT, _prepare=mock.DEFAULT, _stop=mock.DEFAULT)
176 _run=mock.DEFAULT, _prepare=mock.DEFAULT, _stop=mock.DEFAULT)
175
177
176 with patchers as mocks:
178 with patchers as mocks:
177 daemon = hooks_daemon.ThreadedHookCallbackDaemon()
179 daemon = http_hooks_deamon.ThreadedHookCallbackDaemon()
178 with daemon as daemon_context:
180 with daemon as daemon_context:
179 assert mocks['_stop'].call_count == 0
181 assert mocks['_stop'].call_count == 0
180
182
@@ -185,46 +187,47 b' class ThreadedHookCallbackDaemon(object)'
185 class TestHttpHooksCallbackDaemon(object):
187 class TestHttpHooksCallbackDaemon(object):
186 def test_hooks_callback_generates_new_port(self, caplog):
188 def test_hooks_callback_generates_new_port(self, caplog):
187 with caplog.at_level(logging.DEBUG):
189 with caplog.at_level(logging.DEBUG):
188 daemon = hooks_daemon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881)
190 daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881)
189 assert daemon._daemon.server_address == ('127.0.0.1', 8881)
191 assert daemon._daemon.server_address == ('127.0.0.1', 8881)
190
192
191 with caplog.at_level(logging.DEBUG):
193 with caplog.at_level(logging.DEBUG):
192 daemon = hooks_daemon.HttpHooksCallbackDaemon(host=None, port=None)
194 daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host=None, port=None)
193 assert daemon._daemon.server_address[1] in range(0, 66000)
195 assert daemon._daemon.server_address[1] in range(0, 66000)
194 assert daemon._daemon.server_address[0] != '127.0.0.1'
196 assert daemon._daemon.server_address[0] != '127.0.0.1'
195
197
196 def test_prepare_inits_daemon_variable(self, tcp_server, caplog):
198 def test_prepare_inits_daemon_variable(self, tcp_server, caplog):
197 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
199 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
198 daemon = hooks_daemon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881)
200 daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881)
199 assert daemon._daemon == tcp_server
201 assert daemon._daemon == tcp_server
200
202
201 _, port = tcp_server.server_address
203 _, port = tcp_server.server_address
202
204
203 msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \
205 msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \
204 f"hook object: <class 'rhodecode.lib.hooks_daemon.HooksHttpHandler'>"
206 f"hook object: <class 'rhodecode.lib.hook_daemon.http_hooks_deamon.HooksHttpHandler'>"
205 assert_message_in_log(
207 assert_message_in_log(
206 caplog.records, msg, levelno=logging.DEBUG, module='hooks_daemon')
208 caplog.records, msg, levelno=logging.DEBUG, module='http_hooks_deamon')
207
209
208 def test_prepare_inits_hooks_uri_and_logs_it(
210 def test_prepare_inits_hooks_uri_and_logs_it(
209 self, tcp_server, caplog):
211 self, tcp_server, caplog):
210 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
212 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
211 daemon = hooks_daemon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881)
213 daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881)
212
214
213 _, port = tcp_server.server_address
215 _, port = tcp_server.server_address
214 expected_uri = '{}:{}'.format('127.0.0.1', port)
216 expected_uri = '{}:{}'.format('127.0.0.1', port)
215 assert daemon.hooks_uri == expected_uri
217 assert daemon.hooks_uri == expected_uri
216
218
217 msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \
219 msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \
218 f"hook object: <class 'rhodecode.lib.hooks_daemon.HooksHttpHandler'>"
220 f"hook object: <class 'rhodecode.lib.hook_daemon.http_hooks_deamon.HooksHttpHandler'>"
221
219 assert_message_in_log(
222 assert_message_in_log(
220 caplog.records, msg,
223 caplog.records, msg,
221 levelno=logging.DEBUG, module='hooks_daemon')
224 levelno=logging.DEBUG, module='http_hooks_deamon')
222
225
223 def test_run_creates_a_thread(self, tcp_server):
226 def test_run_creates_a_thread(self, tcp_server):
224 thread = mock.Mock()
227 thread = mock.Mock()
225
228
226 with self._tcp_patcher(tcp_server):
229 with self._tcp_patcher(tcp_server):
227 daemon = hooks_daemon.HttpHooksCallbackDaemon()
230 daemon = http_hooks_deamon.HttpHooksCallbackDaemon()
228
231
229 with self._thread_patcher(thread) as thread_mock:
232 with self._thread_patcher(thread) as thread_mock:
230 daemon._run()
233 daemon._run()
@@ -238,7 +241,7 b' class TestHttpHooksCallbackDaemon(object'
238 def test_run_logs(self, tcp_server, caplog):
241 def test_run_logs(self, tcp_server, caplog):
239
242
240 with self._tcp_patcher(tcp_server):
243 with self._tcp_patcher(tcp_server):
241 daemon = hooks_daemon.HttpHooksCallbackDaemon()
244 daemon = http_hooks_deamon.HttpHooksCallbackDaemon()
242
245
243 with self._thread_patcher(mock.Mock()), caplog.at_level(logging.DEBUG):
246 with self._thread_patcher(mock.Mock()), caplog.at_level(logging.DEBUG):
244 daemon._run()
247 daemon._run()
@@ -246,13 +249,13 b' class TestHttpHooksCallbackDaemon(object'
246 assert_message_in_log(
249 assert_message_in_log(
247 caplog.records,
250 caplog.records,
248 'Running thread-based loop of callback daemon in background',
251 'Running thread-based loop of callback daemon in background',
249 levelno=logging.DEBUG, module='hooks_daemon')
252 levelno=logging.DEBUG, module='http_hooks_deamon')
250
253
251 def test_stop_cleans_up_the_connection(self, tcp_server, caplog):
254 def test_stop_cleans_up_the_connection(self, tcp_server, caplog):
252 thread = mock.Mock()
255 thread = mock.Mock()
253
256
254 with self._tcp_patcher(tcp_server):
257 with self._tcp_patcher(tcp_server):
255 daemon = hooks_daemon.HttpHooksCallbackDaemon()
258 daemon = http_hooks_deamon.HttpHooksCallbackDaemon()
256
259
257 with self._thread_patcher(thread), caplog.at_level(logging.DEBUG):
260 with self._thread_patcher(thread), caplog.at_level(logging.DEBUG):
258 with daemon:
261 with daemon:
@@ -266,18 +269,19 b' class TestHttpHooksCallbackDaemon(object'
266
269
267 assert_message_in_log(
270 assert_message_in_log(
268 caplog.records, 'Waiting for background thread to finish.',
271 caplog.records, 'Waiting for background thread to finish.',
269 levelno=logging.DEBUG, module='hooks_daemon')
272 levelno=logging.DEBUG, module='http_hooks_deamon')
270
273
271 def _tcp_patcher(self, tcp_server):
274 def _tcp_patcher(self, tcp_server):
272 return mock.patch.object(
275 return mock.patch.object(
273 hooks_daemon, 'TCPServer', return_value=tcp_server)
276 http_hooks_deamon, 'TCPServer', return_value=tcp_server)
274
277
275 def _thread_patcher(self, thread):
278 def _thread_patcher(self, thread):
276 return mock.patch.object(
279 return mock.patch.object(
277 hooks_daemon.threading, 'Thread', return_value=thread)
280 http_hooks_deamon.threading, 'Thread', return_value=thread)
278
281
279
282
280 class TestPrepareHooksDaemon(object):
283 class TestPrepareHooksDaemon(object):
284
281 @pytest.mark.parametrize('protocol', ('celery',))
285 @pytest.mark.parametrize('protocol', ('celery',))
282 def test_returns_celery_hooks_callback_daemon_when_celery_protocol_specified(
286 def test_returns_celery_hooks_callback_daemon_when_celery_protocol_specified(
283 self, protocol):
287 self, protocol):
@@ -286,12 +290,12 b' class TestPrepareHooksDaemon(object):'
286 "celery.result_backend = redis://redis/0")
290 "celery.result_backend = redis://redis/0")
287 temp_file.flush()
291 temp_file.flush()
288 expected_extras = {'config': temp_file.name}
292 expected_extras = {'config': temp_file.name}
289 callback, extras = hooks_daemon.prepare_callback_daemon(
293 callback, extras = hook_base.prepare_callback_daemon(
290 expected_extras, protocol=protocol, host='')
294 expected_extras, protocol=protocol, host='')
291 assert isinstance(callback, hooks_daemon.CeleryHooksCallbackDaemon)
295 assert isinstance(callback, celery_hooks_deamon.CeleryHooksCallbackDaemon)
292
296
293 @pytest.mark.parametrize('protocol, expected_class', (
297 @pytest.mark.parametrize('protocol, expected_class', (
294 ('http', hooks_daemon.HttpHooksCallbackDaemon),
298 ('http', http_hooks_deamon.HttpHooksCallbackDaemon),
295 ))
299 ))
296 def test_returns_real_hooks_callback_daemon_when_protocol_is_specified(
300 def test_returns_real_hooks_callback_daemon_when_protocol_is_specified(
297 self, protocol, expected_class):
301 self, protocol, expected_class):
@@ -300,9 +304,13 b' class TestPrepareHooksDaemon(object):'
300 'txn_id': 'txnid2',
304 'txn_id': 'txnid2',
301 'hooks_protocol': protocol.lower(),
305 'hooks_protocol': protocol.lower(),
302 'task_backend': '',
306 'task_backend': '',
303 'task_queue': ''
307 'task_queue': '',
308 'repo_store': '/var/opt/rhodecode_repo_store',
309 'repository': 'rhodecode',
304 }
310 }
305 callback, extras = hooks_daemon.prepare_callback_daemon(
311 from rhodecode import CONFIG
312 CONFIG['vcs.svn.redis_conn'] = 'redis://redis:6379/0'
313 callback, extras = hook_base.prepare_callback_daemon(
306 expected_extras.copy(), protocol=protocol, host='127.0.0.1',
314 expected_extras.copy(), protocol=protocol, host='127.0.0.1',
307 txn_id='txnid2')
315 txn_id='txnid2')
308 assert isinstance(callback, expected_class)
316 assert isinstance(callback, expected_class)
@@ -321,7 +329,7 b' class TestPrepareHooksDaemon(object):'
321 'hooks_protocol': protocol.lower()
329 'hooks_protocol': protocol.lower()
322 }
330 }
323 with pytest.raises(Exception):
331 with pytest.raises(Exception):
324 callback, extras = hooks_daemon.prepare_callback_daemon(
332 callback, extras = hook_base.prepare_callback_daemon(
325 expected_extras.copy(),
333 expected_extras.copy(),
326 protocol=protocol, host='127.0.0.1')
334 protocol=protocol, host='127.0.0.1')
327
335
@@ -589,17 +589,6 b' class TestUpdateGlobalSslSetting(object)'
589 model.global_settings, 'web', 'push_ssl', value='False')
589 model.global_settings, 'web', 'push_ssl', value='False')
590
590
591
591
592 class TestUpdateGlobalPathSetting(object):
593 def test_updates_global_path_settings(self):
594 model = VcsSettingsModel()
595 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
596 model.update_global_path_setting('False')
597 Session().commit()
598
599 create_mock.assert_called_once_with(
600 model.global_settings, 'paths', '/', value='False')
601
602
603 class TestCreateOrUpdateGlobalHgSettings(object):
592 class TestCreateOrUpdateGlobalHgSettings(object):
604 FORM_DATA = {
593 FORM_DATA = {
605 'extensions_largefiles': False,
594 'extensions_largefiles': False,
@@ -1004,21 +993,6 b' class TestGetSvnPatterns(object):'
1004 settings_mock.assert_called_once_with(*args)
993 settings_mock.assert_called_once_with(*args)
1005
994
1006
995
1007 class TestGetReposLocation(object):
1008 def test_returns_repos_location(self, repo_stub):
1009 model = VcsSettingsModel()
1010
1011 result_mock = mock.Mock()
1012 result_mock.ui_value = '/tmp'
1013
1014 with mock.patch.object(model, 'global_settings') as settings_mock:
1015 settings_mock.get_ui_by_key.return_value = result_mock
1016 result = model.get_repos_location()
1017
1018 settings_mock.get_ui_by_key.assert_called_once_with('/')
1019 assert result == '/tmp'
1020
1021
1022 class TestCreateOrUpdateRepoSettings(object):
996 class TestCreateOrUpdateRepoSettings(object):
1023 FORM_DATA = {
997 FORM_DATA = {
1024 'inherit_global_settings': False,
998 'inherit_global_settings': False,
@@ -121,10 +121,10 b' class TestRepoModel(object):'
121 def test_create_filesystem_repo_installs_hooks(self, tmpdir, backend):
121 def test_create_filesystem_repo_installs_hooks(self, tmpdir, backend):
122 repo = backend.create_repo()
122 repo = backend.create_repo()
123 repo_name = repo.repo_name
123 repo_name = repo.repo_name
124 model = RepoModel()
124 with mock.patch('rhodecode.model.repo.RepoModel.repos_path',
125 repo_location = tempfile.mkdtemp()
125 new_callable=mock.PropertyMock) as mocked_models_property:
126 model.repos_path = repo_location
126 mocked_models_property.return_value = tempfile.mkdtemp()
127 repo = model._create_filesystem_repo(
127 repo = RepoModel()._create_filesystem_repo(
128 repo_name, backend.alias, repo_group='', clone_uri=None)
128 repo_name, backend.alias, repo_group='', clone_uri=None)
129
129
130 hooks = {
130 hooks = {
@@ -66,7 +66,6 b' prefix = /'
66 ;can be overridden by
66 ;can be overridden by
67 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
67 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
68
68
69 is_test = True
70 use = egg:rhodecode-enterprise-ce
69 use = egg:rhodecode-enterprise-ce
71
70
72 ; enable proxy prefix middleware, defined above
71 ; enable proxy prefix middleware, defined above
@@ -105,6 +104,12 b' startup.import_repos = true'
105 ; SSH calls. Set this for events to receive proper url for SSH calls.
104 ; SSH calls. Set this for events to receive proper url for SSH calls.
106 app.base_url = http://rhodecode.local
105 app.base_url = http://rhodecode.local
107
106
107 ; Host at which the Service API is running.
108 app.service_api.host = http://rhodecode.local:10020
109
110 ; Secret for Service API authentication.
111 app.service_api.token =
112
108 ; Unique application ID. Should be a random unique string for security.
113 ; Unique application ID. Should be a random unique string for security.
109 app_instance_uuid = rc-production
114 app_instance_uuid = rc-production
110
115
@@ -207,8 +212,8 b' auth_ret_code_detection = false'
207 ; codes don't break the transactions while 4XX codes do
212 ; codes don't break the transactions while 4XX codes do
208 lock_ret_code = 423
213 lock_ret_code = 423
209
214
210 ; allows to change the repository location in settings page
215 ; Filesystem location were repositories should be stored
211 allow_repo_location_change = true
216 repo_store.path = /var/opt/rhodecode_repo_store
212
217
213 ; allows to setup custom hooks in settings page
218 ; allows to setup custom hooks in settings page
214 allow_custom_hooks_settings = true
219 allow_custom_hooks_settings = true
@@ -250,23 +255,72 b' file_store.enabled = true'
250 ; Storage backend, available options are: local
255 ; Storage backend, available options are: local
251 file_store.backend = local
256 file_store.backend = local
252
257
253 ; path to store the uploaded binaries
258 ; path to store the uploaded binaries and artifacts
254 file_store.storage_path = %(here)s/data/file_store
259 file_store.storage_path = /var/opt/rhodecode_data/file_store
260
261
262 ; Redis url to acquire/check generation of archives locks
263 archive_cache.locking.url = redis://redis:6379/1
264
265 ; Storage backend, only 'filesystem' and 'objectstore' are available now
266 archive_cache.backend.type = filesystem
267
268 ; url for s3 compatible storage that allows to upload artifacts
269 ; e.g http://minio:9000
270 archive_cache.objectstore.url = http://s3-minio:9000
271
272 ; key for s3 auth
273 archive_cache.objectstore.key = key
274
275 ; secret for s3 auth
276 archive_cache.objectstore.secret = secret
255
277
256 ; Uncomment and set this path to control settings for archive download cache.
278 ;region for s3 storage
279 archive_cache.objectstore.region = eu-central-1
280
281 ; number of sharded buckets to create to distribute archives across
282 ; default is 8 shards
283 archive_cache.objectstore.bucket_shards = 8
284
285 ; a top-level bucket to put all other shards in
286 ; objects will be stored in rhodecode-archive-cache/shard-N based on the bucket_shards number
287 archive_cache.objectstore.bucket = rhodecode-archive-cache
288
289 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
290 archive_cache.objectstore.retry = false
291
292 ; number of seconds to wait for next try using retry
293 archive_cache.objectstore.retry_backoff = 1
294
295 ; how many tries do do a retry fetch from this backend
296 archive_cache.objectstore.retry_attempts = 10
297
298 ; Default is $cache_dir/archive_cache if not set
257 ; Generated repo archives will be cached at this location
299 ; Generated repo archives will be cached at this location
258 ; and served from the cache during subsequent requests for the same archive of
300 ; and served from the cache during subsequent requests for the same archive of
259 ; the repository. This path is important to be shared across filesystems and with
301 ; the repository. This path is important to be shared across filesystems and with
260 ; RhodeCode and vcsserver
302 ; RhodeCode and vcsserver
261
303 archive_cache.filesystem.store_dir = %(here)s/rc-tests/archive_cache
262 ; Default is $cache_dir/archive_cache if not set
263 archive_cache.store_dir = /tmp/rc-test-data/archive_cache
264
304
265 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
305 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
266 archive_cache.cache_size_gb = 10
306 archive_cache.filesystem.cache_size_gb = 2
307
308 ; Eviction policy used to clear out after cache_size_gb limit is reached
309 archive_cache.filesystem.eviction_policy = least-recently-stored
267
310
268 ; By default cache uses sharding technique, this specifies how many shards are there
311 ; By default cache uses sharding technique, this specifies how many shards are there
269 archive_cache.cache_shards = 10
312 ; default is 8 shards
313 archive_cache.filesystem.cache_shards = 8
314
315 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
316 archive_cache.filesystem.retry = false
317
318 ; number of seconds to wait for next try using retry
319 archive_cache.filesystem.retry_backoff = 1
320
321 ; how many tries do do a retry fetch from this backend
322 archive_cache.filesystem.retry_attempts = 10
323
270
324
271 ; #############
325 ; #############
272 ; CELERY CONFIG
326 ; CELERY CONFIG
@@ -280,7 +334,10 b' use_celery = false'
280 #celerybeat-schedule.path =
334 #celerybeat-schedule.path =
281
335
282 ; connection url to the message broker (default redis)
336 ; connection url to the message broker (default redis)
283 celery.broker_url = redis://localhost:6379/8
337 celery.broker_url = redis://redis:6379/8
338
339 ; results backend to get results for (default redis)
340 celery.result_backend = redis://redis:6379/8
284
341
285 ; rabbitmq example
342 ; rabbitmq example
286 #celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
343 #celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
@@ -289,7 +346,8 b' celery.broker_url = redis://localhost:63'
289 celery.max_tasks_per_child = 20
346 celery.max_tasks_per_child = 20
290
347
291 ; tasks will never be sent to the queue, but executed locally instead.
348 ; tasks will never be sent to the queue, but executed locally instead.
292 celery.task_always_eager = false
349 celery.task_always_eager = true
350 celery.task_store_eager_result = true
293
351
294 ; #############
352 ; #############
295 ; DOGPILE CACHE
353 ; DOGPILE CACHE
@@ -326,7 +384,7 b' rc_cache.cache_repo_longterm.max_size = '
326 rc_cache.cache_general.backend = dogpile.cache.rc.file_namespace
384 rc_cache.cache_general.backend = dogpile.cache.rc.file_namespace
327 rc_cache.cache_general.expiration_time = 43200
385 rc_cache.cache_general.expiration_time = 43200
328 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
386 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
329 rc_cache.cache_general.arguments.filename = %(here)s/cache-backend/cache_general_db
387 rc_cache.cache_general.arguments.filename = %(here)s/rc-tests/cache-backend/cache_general_db
330
388
331 ; alternative `cache_general` redis backend with distributed lock
389 ; alternative `cache_general` redis backend with distributed lock
332 #rc_cache.cache_general.backend = dogpile.cache.rc.redis
390 #rc_cache.cache_general.backend = dogpile.cache.rc.redis
@@ -353,7 +411,7 b' rc_cache.cache_general.arguments.filenam'
353 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
411 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
354 rc_cache.cache_perms.expiration_time = 0
412 rc_cache.cache_perms.expiration_time = 0
355 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
413 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
356 rc_cache.cache_perms.arguments.filename = %(here)s/cache-backend/cache_perms_db
414 rc_cache.cache_perms.arguments.filename = %(here)s/rc-tests/cache-backend/cache_perms_db
357
415
358 ; alternative `cache_perms` redis backend with distributed lock
416 ; alternative `cache_perms` redis backend with distributed lock
359 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
417 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
@@ -380,7 +438,7 b' rc_cache.cache_perms.arguments.filename '
380 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
438 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
381 rc_cache.cache_repo.expiration_time = 2592000
439 rc_cache.cache_repo.expiration_time = 2592000
382 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
440 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
383 rc_cache.cache_repo.arguments.filename = %(here)s/cache-backend/cache_repo_db
441 rc_cache.cache_repo.arguments.filename = %(here)s/rc-tests/cache-backend/cache_repo_db
384
442
385 ; alternative `cache_repo` redis backend with distributed lock
443 ; alternative `cache_repo` redis backend with distributed lock
386 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
444 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
@@ -404,14 +462,14 b' rc_cache.cache_repo.arguments.filename ='
404 ; ##############
462 ; ##############
405
463
406 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
464 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
407 ; types are file, ext:redis, ext:database, ext:memcached, and memory (default if not specified).
465 ; types are file, ext:redis, ext:database, ext:memcached
408 ; Fastest ones are Redis and ext:database
466 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
409 beaker.session.type = file
467 beaker.session.type = file
410 beaker.session.data_dir = %(here)s/rc-tests/data/sessions
468 beaker.session.data_dir = %(here)s/rc-tests/data/sessions
411
469
412 ; Redis based sessions
470 ; Redis based sessions
413 #beaker.session.type = ext:redis
471 #beaker.session.type = ext:redis
414 #beaker.session.url = redis://127.0.0.1:6379/2
472 #beaker.session.url = redis://redis:6379/2
415
473
416 ; DB based session, fast, and allows easy management over logged in users
474 ; DB based session, fast, and allows easy management over logged in users
417 #beaker.session.type = ext:database
475 #beaker.session.type = ext:database
@@ -423,7 +481,7 b' beaker.session.data_dir = %(here)s/rc-te'
423
481
424 beaker.session.key = rhodecode
482 beaker.session.key = rhodecode
425 beaker.session.secret = test-rc-uytcxaz
483 beaker.session.secret = test-rc-uytcxaz
426 beaker.session.lock_dir = %(here)s/data/sessions/lock
484 beaker.session.lock_dir = %(here)s/rc-tests/data/sessions/lock
427
485
428 ; Secure encrypted cookie. Requires AES and AES python libraries
486 ; Secure encrypted cookie. Requires AES and AES python libraries
429 ; you must disable beaker.session.secret to use this
487 ; you must disable beaker.session.secret to use this
@@ -441,9 +499,6 b' beaker.session.httponly = true'
441 ; Set https secure cookie
499 ; Set https secure cookie
442 beaker.session.secure = false
500 beaker.session.secure = false
443
501
444 ## auto save the session to not to use .save()
445 beaker.session.auto = false
446
447 ; default cookie expiration time in seconds, set to `true` to set expire
502 ; default cookie expiration time in seconds, set to `true` to set expire
448 ; at browser close
503 ; at browser close
449 #beaker.session.cookie_expires = 3600
504 #beaker.session.cookie_expires = 3600
@@ -458,7 +513,7 b' beaker.session.auto = false'
458 ; WHOOSH Backend, doesn't require additional services to run
513 ; WHOOSH Backend, doesn't require additional services to run
459 ; it works good with few dozen repos
514 ; it works good with few dozen repos
460 search.module = rhodecode.lib.index.whoosh
515 search.module = rhodecode.lib.index.whoosh
461 search.location = %(here)s/data/index
516 search.location = %(here)s/rc-tests/data/index
462
517
463 ; ####################
518 ; ####################
464 ; CHANNELSTREAM CONFIG
519 ; CHANNELSTREAM CONFIG
@@ -470,15 +525,15 b' search.location = %(here)s/data/index'
470 channelstream.enabled = false
525 channelstream.enabled = false
471
526
472 ; server address for channelstream server on the backend
527 ; server address for channelstream server on the backend
473 channelstream.server = 127.0.0.1:9800
528 channelstream.server = channelstream:9800
474
529
475 ; location of the channelstream server from outside world
530 ; location of the channelstream server from outside world
476 ; use ws:// for http or wss:// for https. This address needs to be handled
531 ; use ws:// for http or wss:// for https. This address needs to be handled
477 ; by external HTTP server such as Nginx or Apache
532 ; by external HTTP server such as Nginx or Apache
478 ; see Nginx/Apache configuration examples in our docs
533 ; see Nginx/Apache configuration examples in our docs
479 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
534 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
480 channelstream.secret = secret
535 channelstream.secret = ENV_GENERATED
481 channelstream.history.location = %(here)s/channelstream_history
536 channelstream.history.location = %(here)s/rc-tests/channelstream_history
482
537
483 ; Internal application path that Javascript uses to connect into.
538 ; Internal application path that Javascript uses to connect into.
484 ; If you use proxy-prefix the prefix should be added before /_channelstream
539 ; If you use proxy-prefix the prefix should be added before /_channelstream
@@ -495,7 +550,7 b' channelstream.proxy_path = /_channelstre'
495 ; pymysql is an alternative driver for MySQL, use in case of problems with default one
550 ; pymysql is an alternative driver for MySQL, use in case of problems with default one
496 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
551 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
497
552
498 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode_test.db?timeout=30
553 sqlalchemy.db1.url = sqlite:///%(here)s/rc-tests/rhodecode_test.db?timeout=30
499
554
500 ; see sqlalchemy docs for other advanced settings
555 ; see sqlalchemy docs for other advanced settings
501 ; print the sql statements to output
556 ; print the sql statements to output
@@ -537,6 +592,7 b' vcs.scm_app_implementation = http'
537
592
538 ; Push/Pull operations hooks protocol, available options are:
593 ; Push/Pull operations hooks protocol, available options are:
539 ; `http` - use http-rpc backend (default)
594 ; `http` - use http-rpc backend (default)
595 ; `celery` - use celery based hooks
540 vcs.hooks.protocol = http
596 vcs.hooks.protocol = http
541
597
542 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
598 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
@@ -556,11 +612,6 b' vcs.backends = hg, git, svn'
556 ; Wait this number of seconds before killing connection to the vcsserver
612 ; Wait this number of seconds before killing connection to the vcsserver
557 vcs.connection_timeout = 3600
613 vcs.connection_timeout = 3600
558
614
559 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
560 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
561 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
562 #vcs.svn.compatible_version = 1.8
563
564 ; Cache flag to cache vcsserver remote calls locally
615 ; Cache flag to cache vcsserver remote calls locally
565 ; It uses cache_region `cache_repo`
616 ; It uses cache_region `cache_repo`
566 vcs.methods.cache = false
617 vcs.methods.cache = false
@@ -570,6 +621,17 b' vcs.methods.cache = false'
570 ; Maps RhodeCode repo groups into SVN paths for Apache
621 ; Maps RhodeCode repo groups into SVN paths for Apache
571 ; ####################################################
622 ; ####################################################
572
623
624 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
625 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
626 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
627 #vcs.svn.compatible_version = 1.8
628
629 ; Enable SVN proxy of requests over HTTP
630 vcs.svn.proxy.enabled = true
631
632 ; host to connect to running SVN subsystem
633 vcs.svn.proxy.host = http://svn:8090
634
573 ; Enable or disable the config file generation.
635 ; Enable or disable the config file generation.
574 svn.proxy.generate_config = false
636 svn.proxy.generate_config = false
575
637
@@ -577,7 +639,7 b' svn.proxy.generate_config = false'
577 svn.proxy.list_parent_path = true
639 svn.proxy.list_parent_path = true
578
640
579 ; Set location and file name of generated config file.
641 ; Set location and file name of generated config file.
580 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
642 svn.proxy.config_file_path = %(here)s/rc-tests/mod_dav_svn.conf
581
643
582 ; alternative mod_dav config template. This needs to be a valid mako template
644 ; alternative mod_dav config template. This needs to be a valid mako template
583 ; Example template can be found in the source code:
645 ; Example template can be found in the source code:
@@ -613,25 +675,26 b' ssh.generate_authorized_keyfile = true'
613 ; Path to the authorized_keys file where the generate entries are placed.
675 ; Path to the authorized_keys file where the generate entries are placed.
614 ; It is possible to have multiple key files specified in `sshd_config` e.g.
676 ; It is possible to have multiple key files specified in `sshd_config` e.g.
615 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
677 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
616 ssh.authorized_keys_file_path = %(here)s/rc/authorized_keys_rhodecode
678 ssh.authorized_keys_file_path = %(here)s/rc-tests/authorized_keys_rhodecode
617
679
618 ; Command to execute the SSH wrapper. The binary is available in the
680 ; Command to execute the SSH wrapper. The binary is available in the
619 ; RhodeCode installation directory.
681 ; RhodeCode installation directory.
620 ; e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper
682 ; legacy: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
621 ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper
683 ; new rewrite: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
684 ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
622
685
623 ; Allow shell when executing the ssh-wrapper command
686 ; Allow shell when executing the ssh-wrapper command
624 ssh.wrapper_cmd_allow_shell = false
687 ssh.wrapper_cmd_allow_shell = false
625
688
626 ; Enables logging, and detailed output send back to the client during SSH
689 ; Enables logging, and detailed output send back to the client during SSH
627 ; operations. Useful for debugging, shouldn't be used in production.
690 ; operations. Useful for debugging, shouldn't be used in production.
628 ssh.enable_debug_logging = false
691 ssh.enable_debug_logging = true
629
692
630 ; Paths to binary executable, by default they are the names, but we can
693 ; Paths to binary executable, by default they are the names, but we can
631 ; override them if we want to use a custom one
694 ; override them if we want to use a custom one
632 ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg
695 ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg
633 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
696 ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git
634 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
697 ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve
635
698
636 ; Enables SSH key generator web interface. Disabling this still allows users
699 ; Enables SSH key generator web interface. Disabling this still allows users
637 ; to add their own keys.
700 ; to add their own keys.
@@ -106,6 +106,8 b' def get_url_defs():'
106 + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}",
106 + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}",
107 "login": ADMIN_PREFIX + "/login",
107 "login": ADMIN_PREFIX + "/login",
108 "logout": ADMIN_PREFIX + "/logout",
108 "logout": ADMIN_PREFIX + "/logout",
109 "setup_2fa": ADMIN_PREFIX + "/setup_2fa",
110 "check_2fa": ADMIN_PREFIX + "/check_2fa",
109 "register": ADMIN_PREFIX + "/register",
111 "register": ADMIN_PREFIX + "/register",
110 "reset_password": ADMIN_PREFIX + "/password_reset",
112 "reset_password": ADMIN_PREFIX + "/password_reset",
111 "reset_password_confirmation": ADMIN_PREFIX + "/password_reset_confirmation",
113 "reset_password_confirmation": ADMIN_PREFIX + "/password_reset_confirmation",
@@ -250,6 +252,7 b' def get_url_defs():'
250 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
252 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
251 "bookmarks_home": "/{repo_name}/bookmarks",
253 "bookmarks_home": "/{repo_name}/bookmarks",
252 "branches_home": "/{repo_name}/branches",
254 "branches_home": "/{repo_name}/branches",
255 "branch_remove": "/{repo_name}/branches/{branch_name}/remove",
253 "tags_home": "/{repo_name}/tags",
256 "tags_home": "/{repo_name}/tags",
254 "repo_changelog": "/{repo_name}/changelog",
257 "repo_changelog": "/{repo_name}/changelog",
255 "repo_commits": "/{repo_name}/commits",
258 "repo_commits": "/{repo_name}/commits",
@@ -143,13 +143,14 b' class RcVCSServer(ServerBase):'
143 log_file_name = 'rc-vcsserver.log'
143 log_file_name = 'rc-vcsserver.log'
144 status_url_tmpl = 'http://{host}:{port}/status'
144 status_url_tmpl = 'http://{host}:{port}/status'
145
145
146 def __init__(self, config_file, log_file=None, workers='2'):
146 def __init__(self, config_file, log_file=None, workers='3'):
147 super(RcVCSServer, self).__init__(config_file, log_file)
147 super(RcVCSServer, self).__init__(config_file, log_file)
148 self._args = [
148 self._args = [
149 'gunicorn',
149 'gunicorn',
150 '--bind', self.bind_addr,
150 '--bind', self.bind_addr,
151 '--worker-class', 'gevent',
151 '--worker-class', 'sync',
152 '--backlog', '16',
152 '--threads', '1',
153 '--backlog', '8',
153 '--timeout', '300',
154 '--timeout', '300',
154 '--workers', workers,
155 '--workers', workers,
155 '--paste', self.config_file]
156 '--paste', self.config_file]
@@ -180,13 +181,14 b' class RcWebServer(ServerBase):'
180 log_file_name = 'rc-web.log'
181 log_file_name = 'rc-web.log'
181 status_url_tmpl = 'http://{host}:{port}/_admin/ops/ping'
182 status_url_tmpl = 'http://{host}:{port}/_admin/ops/ping'
182
183
183 def __init__(self, config_file, log_file=None, workers='1'):
184 def __init__(self, config_file, log_file=None, workers='2'):
184 super(RcWebServer, self).__init__(config_file, log_file)
185 super(RcWebServer, self).__init__(config_file, log_file)
185 self._args = [
186 self._args = [
186 'gunicorn',
187 'gunicorn',
187 '--bind', self.bind_addr,
188 '--bind', self.bind_addr,
188 '--worker-class', 'gevent',
189 '--worker-class', 'gthread',
189 '--backlog', '16',
190 '--threads', '4',
191 '--backlog', '8',
190 '--timeout', '300',
192 '--timeout', '300',
191 '--workers', workers,
193 '--workers', workers,
192 '--paste', self.config_file]
194 '--paste', self.config_file]
@@ -219,3 +221,11 b' class RcWebServer(ServerBase):'
219 params.update(**kwargs)
221 params.update(**kwargs)
220 _url = f"http://{params['user']}:{params['passwd']}@{params['host']}/{params['cloned_repo']}"
222 _url = f"http://{params['user']}:{params['passwd']}@{params['host']}/{params['cloned_repo']}"
221 return _url
223 return _url
224
225 def repo_clone_credentials(self, **kwargs):
226 params = {
227 'user': TEST_USER_ADMIN_LOGIN,
228 'passwd': TEST_USER_ADMIN_PASS,
229 }
230 params.update(**kwargs)
231 return params['user'], params['passwd']
@@ -28,7 +28,7 b' import mock'
28 import pytest
28 import pytest
29
29
30 import rhodecode
30 import rhodecode
31 from rhodecode.lib.rc_cache.archive_cache import get_archival_config
31 from rhodecode.lib.archive_cache import get_archival_config
32 from rhodecode.lib.str_utils import ascii_bytes
32 from rhodecode.lib.str_utils import ascii_bytes
33 from rhodecode.lib.vcs.backends import base
33 from rhodecode.lib.vcs.backends import base
34 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError, VCSError
34 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError, VCSError
@@ -969,7 +969,7 b' class TestGitCommit(object):'
969 branches = self.repo.branches
969 branches = self.repo.branches
970
970
971 assert 'unicode' in branches
971 assert 'unicode' in branches
972 assert u'uniçö∂e' in branches
972 assert 'uniçö∂e' in branches
973
973
974 def test_unicode_tag_refs(self):
974 def test_unicode_tag_refs(self):
975 unicode_tags = {
975 unicode_tags = {
@@ -983,7 +983,7 b' class TestGitCommit(object):'
983 tags = self.repo.tags
983 tags = self.repo.tags
984
984
985 assert 'unicode' in tags
985 assert 'unicode' in tags
986 assert u'uniçö∂e' in tags
986 assert 'uniçö∂e' in tags
987
987
988 def test_commit_message_is_unicode(self):
988 def test_commit_message_is_unicode(self):
989 for commit in self.repo:
989 for commit in self.repo:
@@ -145,14 +145,14 b' def test_unicode_refs(vcsbackend, filena'
145 with mock.patch(("rhodecode.lib.vcs.backends.svn.repository"
145 with mock.patch(("rhodecode.lib.vcs.backends.svn.repository"
146 ".SubversionRepository._patterns_from_section"),
146 ".SubversionRepository._patterns_from_section"),
147 return_value=['branches/*']):
147 return_value=['branches/*']):
148 assert u'branches/{0}'.format(branch) in repo.branches
148 assert f'branches/{branch}' in repo.branches
149
149
150
150
151 def test_compatible_version(monkeypatch, vcsbackend):
151 def test_compatible_version(monkeypatch, vcsbackend):
152 monkeypatch.setattr(settings, 'SVN_COMPATIBLE_VERSION', 'pre-1.8-compatible')
152 monkeypatch.setattr(settings, 'SVN_COMPATIBLE_VERSION', 'pre-1.8-compatible')
153 path = vcsbackend.new_repo_path()
153 path = vcsbackend.new_repo_path()
154 SubversionRepository(path, create=True)
154 SubversionRepository(path, create=True)
155 with open('{}/db/format'.format(path)) as f:
155 with open(f'{path}/db/format') as f:
156 first_line = f.readline().strip()
156 first_line = f.readline().strip()
157 assert first_line == '4'
157 assert first_line == '4'
158
158
@@ -26,20 +26,21 b' Base for test suite for making push/pull'
26 to redirect things to stderr instead of stdout.
26 to redirect things to stderr instead of stdout.
27 """
27 """
28
28
29 from os.path import join as jn
29
30 from subprocess import Popen, PIPE
31 import logging
30 import logging
32 import os
31 import os
33 import tempfile
32 import tempfile
33 import subprocess
34
34
35 from rhodecode.lib.str_utils import safe_str
35 from rhodecode.lib.str_utils import safe_str
36 from rhodecode.tests import GIT_REPO, HG_REPO
36 from rhodecode.tests import GIT_REPO, HG_REPO, SVN_REPO
37
37
38 DEBUG = True
38 DEBUG = True
39 RC_LOG = os.path.join(tempfile.gettempdir(), 'rc.log')
39 RC_LOG = os.path.join(tempfile.gettempdir(), 'rc.log')
40 REPO_GROUP = 'a_repo_group'
40 REPO_GROUP = 'a_repo_group'
41 HG_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, HG_REPO)
41 HG_REPO_WITH_GROUP = f'{REPO_GROUP}/{HG_REPO}'
42 GIT_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, GIT_REPO)
42 GIT_REPO_WITH_GROUP = f'{REPO_GROUP}/{GIT_REPO}'
43 SVN_REPO_WITH_GROUP = f'{REPO_GROUP}/{SVN_REPO}'
43
44
44 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
45
46
@@ -65,7 +66,8 b' class Command(object):'
65 if key.startswith('COV_CORE_'):
66 if key.startswith('COV_CORE_'):
66 del env[key]
67 del env[key]
67
68
68 self.process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE,
69 self.process = subprocess.Popen(
70 command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
69 cwd=self.cwd, env=env)
71 cwd=self.cwd, env=env)
70 stdout, stderr = self.process.communicate()
72 stdout, stderr = self.process.communicate()
71
73
@@ -85,12 +87,14 b' def _add_files(vcs, dest, clone_url=None'
85 full_name = 'Marcin Kuźminski'
87 full_name = 'Marcin Kuźminski'
86 email = 'me@email.com'
88 email = 'me@email.com'
87 git_ident = f"git config user.name {full_name} && git config user.email {email}"
89 git_ident = f"git config user.name {full_name} && git config user.email {email}"
88 cwd = path = jn(dest)
90 cwd = path = os.path.join(dest)
89
91
90 tags = tags or []
92 tags = tags or []
91 added_file = jn(path, '{}_setup.py'.format(next(tempfile._RandomNameSequence())))
93 name_sequence = next(tempfile._RandomNameSequence())
92 Command(cwd).execute('touch %s' % added_file)
94 added_file = os.path.join(path, f'{name_sequence}_setup.py')
93 Command(cwd).execute('%s add %s' % (vcs, added_file))
95
96 Command(cwd).execute(f'touch {added_file}')
97 Command(cwd).execute(f'{vcs} add {added_file}')
94 author_str = 'Marcin Kuźminski <me@email.com>'
98 author_str = 'Marcin Kuźminski <me@email.com>'
95
99
96 for i in range(kwargs.get('files_no', 3)):
100 for i in range(kwargs.get('files_no', 3)):
@@ -128,7 +132,7 b' def _add_files_and_push(vcs, dest, clone'
128 vcs is git or hg and defines what VCS we want to make those files for
132 vcs is git or hg and defines what VCS we want to make those files for
129 """
133 """
130 git_ident = "git config user.name Marcin Kuźminski && git config user.email me@email.com"
134 git_ident = "git config user.name Marcin Kuźminski && git config user.email me@email.com"
131 cwd = jn(dest)
135 cwd = os.path.join(dest)
132
136
133 # commit some stuff into this repo
137 # commit some stuff into this repo
134 _add_files(vcs, dest, clone_url, tags, target_branch, new_branch, **kwargs)
138 _add_files(vcs, dest, clone_url, tags, target_branch, new_branch, **kwargs)
@@ -147,12 +151,21 b' def _add_files_and_push(vcs, dest, clone'
147 if new_branch:
151 if new_branch:
148 maybe_new_branch = '--new-branch'
152 maybe_new_branch = '--new-branch'
149 stdout, stderr = Command(cwd).execute(
153 stdout, stderr = Command(cwd).execute(
150 'hg push --traceback --verbose {} -r {} {}'.format(maybe_new_branch, target_branch, clone_url)
154 f'hg push --traceback --verbose {maybe_new_branch} -r {target_branch} {clone_url}'
151 )
155 )
152 elif vcs == 'git':
156 elif vcs == 'git':
153 stdout, stderr = Command(cwd).execute(
157 stdout, stderr = Command(cwd).execute(
154 """{} &&
158 f'{git_ident} && git push --verbose --tags {clone_url} {target_branch}'
155 git push --verbose --tags {} {}""".format(git_ident, clone_url, target_branch)
159 )
160 elif vcs == 'svn':
161 username = kwargs.pop('username', '')
162 password = kwargs.pop('password', '')
163 auth = ''
164 if username and password:
165 auth = f'--username {username} --password {password}'
166
167 stdout, stderr = Command(cwd).execute(
168 f'svn commit --no-auth-cache --non-interactive {auth} -m "pushing to {target_branch}"'
156 )
169 )
157
170
158 return stdout, stderr
171 return stdout, stderr
@@ -179,6 +192,13 b' def _check_proper_hg_push(stdout, stderr'
179 assert 'abort:' not in stderr
192 assert 'abort:' not in stderr
180
193
181
194
195 def _check_proper_svn_push(stdout, stderr):
196 assert 'pushing to' in stdout
197 assert 'searching for changes' in stdout
198
199 assert 'abort:' not in stderr
200
201
182 def _check_proper_clone(stdout, stderr, vcs):
202 def _check_proper_clone(stdout, stderr, vcs):
183 if vcs == 'hg':
203 if vcs == 'hg':
184 assert 'requesting all changes' in stdout
204 assert 'requesting all changes' in stdout
@@ -193,3 +213,8 b' def _check_proper_clone(stdout, stderr, '
193 assert 'Cloning into' in stderr
213 assert 'Cloning into' in stderr
194 assert 'abort:' not in stderr
214 assert 'abort:' not in stderr
195 assert 'fatal:' not in stderr
215 assert 'fatal:' not in stderr
216
217 if vcs == 'svn':
218 assert 'dupa' in stdout
219
220
@@ -42,7 +42,7 b' from rhodecode.model.db import Repositor'
42 from rhodecode.model.meta import Session
42 from rhodecode.model.meta import Session
43 from rhodecode.integrations.types.webhook import WebhookIntegrationType
43 from rhodecode.integrations.types.webhook import WebhookIntegrationType
44
44
45 from rhodecode.tests import GIT_REPO, HG_REPO
45 from rhodecode.tests import GIT_REPO, HG_REPO, SVN_REPO
46 from rhodecode.tests.conftest import HTTPBIN_DOMAIN, HTTPBIN_POST
46 from rhodecode.tests.conftest import HTTPBIN_DOMAIN, HTTPBIN_POST
47 from rhodecode.tests.fixture import Fixture
47 from rhodecode.tests.fixture import Fixture
48 from rhodecode.tests.server_utils import RcWebServer
48 from rhodecode.tests.server_utils import RcWebServer
@@ -51,13 +51,15 b' from rhodecode.tests.server_utils import'
51 REPO_GROUP = 'a_repo_group'
51 REPO_GROUP = 'a_repo_group'
52 HG_REPO_WITH_GROUP = f'{REPO_GROUP}/{HG_REPO}'
52 HG_REPO_WITH_GROUP = f'{REPO_GROUP}/{HG_REPO}'
53 GIT_REPO_WITH_GROUP = f'{REPO_GROUP}/{GIT_REPO}'
53 GIT_REPO_WITH_GROUP = f'{REPO_GROUP}/{GIT_REPO}'
54 SVN_REPO_WITH_GROUP = f'{REPO_GROUP}/{SVN_REPO}'
54
55
55 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
56
57
57
58
58 def check_httpbin_connection():
59 def check_httpbin_connection():
60 log.debug('Checking if HTTPBIN_DOMAIN: %s is available', HTTPBIN_DOMAIN)
59 try:
61 try:
60 response = requests.get(HTTPBIN_DOMAIN)
62 response = requests.get(HTTPBIN_DOMAIN, timeout=5)
61 return response.status_code == 200
63 return response.status_code == 200
62 except Exception as e:
64 except Exception as e:
63 print(e)
65 print(e)
@@ -102,11 +104,15 b' def repos(request, db_connection):'
102 fixture.create_fork(GIT_REPO, GIT_REPO,
104 fixture.create_fork(GIT_REPO, GIT_REPO,
103 repo_name_full=GIT_REPO_WITH_GROUP,
105 repo_name_full=GIT_REPO_WITH_GROUP,
104 repo_group=repo_group_id)
106 repo_group=repo_group_id)
107 fixture.create_fork(SVN_REPO, SVN_REPO,
108 repo_name_full=SVN_REPO_WITH_GROUP,
109 repo_group=repo_group_id)
105
110
106 @request.addfinalizer
111 @request.addfinalizer
107 def cleanup():
112 def cleanup():
108 fixture.destroy_repo(HG_REPO_WITH_GROUP)
113 fixture.destroy_repo(HG_REPO_WITH_GROUP)
109 fixture.destroy_repo(GIT_REPO_WITH_GROUP)
114 fixture.destroy_repo(GIT_REPO_WITH_GROUP)
115 fixture.destroy_repo(SVN_REPO_WITH_GROUP)
110 fixture.destroy_repo_group(repo_group_id)
116 fixture.destroy_repo_group(repo_group_id)
111
117
112
118
@@ -139,11 +145,11 b' def rc_web_server('
139 """
145 """
140 Run the web server as a subprocess. with its own instance of vcsserver
146 Run the web server as a subprocess. with its own instance of vcsserver
141 """
147 """
142 rcweb_port = available_port_factory()
148 rcweb_port: int = available_port_factory()
143 log.info('Using rcweb ops test port {}'.format(rcweb_port))
149 log.info('Using rcweb ops test port %s', rcweb_port)
144
150
145 vcsserver_port = available_port_factory()
151 vcsserver_port: int = available_port_factory()
146 log.info('Using vcsserver ops test port {}'.format(vcsserver_port))
152 log.info('Using vcsserver ops test port %s', vcsserver_port)
147
153
148 vcs_log = os.path.join(tempfile.gettempdir(), 'rc_op_vcs.log')
154 vcs_log = os.path.join(tempfile.gettempdir(), 'rc_op_vcs.log')
149 vcsserver_factory(
155 vcsserver_factory(
@@ -303,5 +309,3 b' def branch_permission_setter(request):'
303 Session().commit()
309 Session().commit()
304
310
305 return _branch_permissions_setter
311 return _branch_permissions_setter
306
307
@@ -32,7 +32,7 b' from rhodecode.lib.vcs.backends.git.repo'
32 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.lib.vcs.nodes import FileNode
33 from rhodecode.tests import GIT_REPO
33 from rhodecode.tests import GIT_REPO
34 from rhodecode.tests.vcs_operations import Command
34 from rhodecode.tests.vcs_operations import Command
35 from .test_vcs_operations import _check_proper_clone, _check_proper_git_push
35 from .test_vcs_operations_git import _check_proper_clone, _check_proper_git_push
36
36
37
37
38 def test_git_clone_with_small_push_buffer(backend_git, rc_web_server, tmpdir):
38 def test_git_clone_with_small_push_buffer(backend_git, rc_web_server, tmpdir):
@@ -28,47 +28,23 b' Test suite for making push/pull operatio'
28
28
29
29
30 import time
30 import time
31 import logging
32
33 import pytest
31 import pytest
34
32
35 from rhodecode.lib import rc_cache
33 from rhodecode.model.db import Repository, UserIpMap
36 from rhodecode.model.auth_token import AuthTokenModel
37 from rhodecode.model.db import Repository, UserIpMap, CacheKey
38 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
39 from rhodecode.model.repo import RepoModel
35 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.user import UserModel
36 from rhodecode.model.user import UserModel
41 from rhodecode.tests import (GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN)
37 from rhodecode.tests import (GIT_REPO, TEST_USER_ADMIN_LOGIN)
42 from rhodecode.tests.utils import assert_message_in_log
38
43
39
44 from rhodecode.tests.vcs_operations import (
40 from rhodecode.tests.vcs_operations import (
45 Command, _check_proper_clone, _check_proper_git_push,
41 Command, _check_proper_clone, _check_proper_git_push,
46 _add_files_and_push, HG_REPO_WITH_GROUP, GIT_REPO_WITH_GROUP)
42 _add_files_and_push, GIT_REPO_WITH_GROUP)
47
43
48
44
49 @pytest.mark.usefixtures("disable_locking", "disable_anonymous_user")
45 @pytest.mark.usefixtures("disable_locking", "disable_anonymous_user")
50 class TestVCSOperations(object):
46 class TestVCSOperations(object):
51
47
52 def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir):
53 clone_url = rc_web_server.repo_clone_url(HG_REPO)
54 stdout, stderr = Command('/tmp').execute(
55 'hg clone', clone_url, tmpdir.strpath)
56 _check_proper_clone(stdout, stderr, 'hg')
57
58 def test_clone_hg_repo_by_admin_pull_protocol(self, rc_web_server, tmpdir):
59 clone_url = rc_web_server.repo_clone_url(HG_REPO)
60 stdout, stderr = Command('/tmp').execute(
61 'hg clone --pull', clone_url, tmpdir.strpath)
62 _check_proper_clone(stdout, stderr, 'hg')
63
64 def test_clone_hg_repo_by_admin_pull_stream_protocol(self, rc_web_server, tmpdir):
65 clone_url = rc_web_server.repo_clone_url(HG_REPO)
66 stdout, stderr = Command('/tmp').execute(
67 'hg clone --pull --stream', clone_url, tmpdir.strpath)
68 assert 'files to transfer,' in stdout
69 assert 'transferred 1.' in stdout
70 assert '114 files updated,' in stdout
71
72 def test_clone_git_repo_by_admin(self, rc_web_server, tmpdir):
48 def test_clone_git_repo_by_admin(self, rc_web_server, tmpdir):
73 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
49 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
74 cmd = Command('/tmp')
50 cmd = Command('/tmp')
@@ -83,13 +59,6 b' class TestVCSOperations(object):'
83 _check_proper_clone(stdout, stderr, 'git')
59 _check_proper_clone(stdout, stderr, 'git')
84 cmd.assert_returncode_success()
60 cmd.assert_returncode_success()
85
61
86 def test_clone_hg_repo_by_id_by_admin(self, rc_web_server, tmpdir):
87 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
88 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
89 stdout, stderr = Command('/tmp').execute(
90 'hg clone', clone_url, tmpdir.strpath)
91 _check_proper_clone(stdout, stderr, 'hg')
92
93 def test_clone_git_repo_by_id_by_admin(self, rc_web_server, tmpdir):
62 def test_clone_git_repo_by_id_by_admin(self, rc_web_server, tmpdir):
94 repo_id = Repository.get_by_repo_name(GIT_REPO).repo_id
63 repo_id = Repository.get_by_repo_name(GIT_REPO).repo_id
95 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
64 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
@@ -98,12 +67,6 b' class TestVCSOperations(object):'
98 _check_proper_clone(stdout, stderr, 'git')
67 _check_proper_clone(stdout, stderr, 'git')
99 cmd.assert_returncode_success()
68 cmd.assert_returncode_success()
100
69
101 def test_clone_hg_repo_with_group_by_admin(self, rc_web_server, tmpdir):
102 clone_url = rc_web_server.repo_clone_url(HG_REPO_WITH_GROUP)
103 stdout, stderr = Command('/tmp').execute(
104 'hg clone', clone_url, tmpdir.strpath)
105 _check_proper_clone(stdout, stderr, 'hg')
106
107 def test_clone_git_repo_with_group_by_admin(self, rc_web_server, tmpdir):
70 def test_clone_git_repo_with_group_by_admin(self, rc_web_server, tmpdir):
108 clone_url = rc_web_server.repo_clone_url(GIT_REPO_WITH_GROUP)
71 clone_url = rc_web_server.repo_clone_url(GIT_REPO_WITH_GROUP)
109 cmd = Command('/tmp')
72 cmd = Command('/tmp')
@@ -121,11 +84,6 b' class TestVCSOperations(object):'
121 assert 'Cloning into' in stderr
84 assert 'Cloning into' in stderr
122 cmd.assert_returncode_success()
85 cmd.assert_returncode_success()
123
86
124 def test_clone_wrong_credentials_hg(self, rc_web_server, tmpdir):
125 clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!')
126 stdout, stderr = Command('/tmp').execute(
127 'hg clone', clone_url, tmpdir.strpath)
128 assert 'abort: authorization failed' in stderr
129
87
130 def test_clone_wrong_credentials_git(self, rc_web_server, tmpdir):
88 def test_clone_wrong_credentials_git(self, rc_web_server, tmpdir):
131 clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!')
89 clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!')
@@ -139,12 +97,6 b' class TestVCSOperations(object):'
139 'hg clone', clone_url, tmpdir.strpath)
97 'hg clone', clone_url, tmpdir.strpath)
140 assert 'HTTP Error 404: Not Found' in stderr
98 assert 'HTTP Error 404: Not Found' in stderr
141
99
142 def test_clone_hg_repo_as_git(self, rc_web_server, tmpdir):
143 clone_url = rc_web_server.repo_clone_url(HG_REPO)
144 stdout, stderr = Command('/tmp').execute(
145 'git clone', clone_url, tmpdir.strpath)
146 assert 'not found' in stderr
147
148 def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir):
100 def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir):
149 clone_url = rc_web_server.repo_clone_url('trololo')
101 clone_url = rc_web_server.repo_clone_url('trololo')
150 stdout, stderr = Command('/tmp').execute(
102 stdout, stderr = Command('/tmp').execute(
@@ -156,25 +108,11 b' class TestVCSOperations(object):'
156 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
108 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
157 assert 'not found' in stderr
109 assert 'not found' in stderr
158
110
159 def test_clone_hg_with_slashes(self, rc_web_server, tmpdir):
160 clone_url = rc_web_server.repo_clone_url('//' + HG_REPO)
161 stdout, stderr = Command('/tmp').execute('hg clone', clone_url, tmpdir.strpath)
162 assert 'HTTP Error 404: Not Found' in stderr
163
164 def test_clone_git_with_slashes(self, rc_web_server, tmpdir):
111 def test_clone_git_with_slashes(self, rc_web_server, tmpdir):
165 clone_url = rc_web_server.repo_clone_url('//' + GIT_REPO)
112 clone_url = rc_web_server.repo_clone_url('//' + GIT_REPO)
166 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
113 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
167 assert 'not found' in stderr
114 assert 'not found' in stderr
168
115
169 def test_clone_existing_path_hg_not_in_database(
170 self, rc_web_server, tmpdir, fs_repo_only):
171
172 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
173 clone_url = rc_web_server.repo_clone_url(db_name)
174 stdout, stderr = Command('/tmp').execute(
175 'hg clone', clone_url, tmpdir.strpath)
176 assert 'HTTP Error 404: Not Found' in stderr
177
178 def test_clone_existing_path_git_not_in_database(
116 def test_clone_existing_path_git_not_in_database(
179 self, rc_web_server, tmpdir, fs_repo_only):
117 self, rc_web_server, tmpdir, fs_repo_only):
180 db_name = fs_repo_only('not-in-db-git', repo_type='git')
118 db_name = fs_repo_only('not-in-db-git', repo_type='git')
@@ -183,14 +121,6 b' class TestVCSOperations(object):'
183 'git clone', clone_url, tmpdir.strpath)
121 'git clone', clone_url, tmpdir.strpath)
184 assert 'not found' in stderr
122 assert 'not found' in stderr
185
123
186 def test_clone_existing_path_hg_not_in_database_different_scm(
187 self, rc_web_server, tmpdir, fs_repo_only):
188 db_name = fs_repo_only('not-in-db-git', repo_type='git')
189 clone_url = rc_web_server.repo_clone_url(db_name)
190 stdout, stderr = Command('/tmp').execute(
191 'hg clone', clone_url, tmpdir.strpath)
192 assert 'HTTP Error 404: Not Found' in stderr
193
194 def test_clone_existing_path_git_not_in_database_different_scm(
124 def test_clone_existing_path_git_not_in_database_different_scm(
195 self, rc_web_server, tmpdir, fs_repo_only):
125 self, rc_web_server, tmpdir, fs_repo_only):
196 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
126 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
@@ -199,17 +129,6 b' class TestVCSOperations(object):'
199 'git clone', clone_url, tmpdir.strpath)
129 'git clone', clone_url, tmpdir.strpath)
200 assert 'not found' in stderr
130 assert 'not found' in stderr
201
131
202 def test_clone_non_existing_store_path_hg(self, rc_web_server, tmpdir, user_util):
203 repo = user_util.create_repo()
204 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
205
206 # Damage repo by removing it's folder
207 RepoModel()._delete_filesystem_repo(repo)
208
209 stdout, stderr = Command('/tmp').execute(
210 'hg clone', clone_url, tmpdir.strpath)
211 assert 'HTTP Error 404: Not Found' in stderr
212
213 def test_clone_non_existing_store_path_git(self, rc_web_server, tmpdir, user_util):
132 def test_clone_non_existing_store_path_git(self, rc_web_server, tmpdir, user_util):
214 repo = user_util.create_repo(repo_type='git')
133 repo = user_util.create_repo(repo_type='git')
215 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
134 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
@@ -221,17 +140,6 b' class TestVCSOperations(object):'
221 'git clone', clone_url, tmpdir.strpath)
140 'git clone', clone_url, tmpdir.strpath)
222 assert 'not found' in stderr
141 assert 'not found' in stderr
223
142
224 def test_push_new_file_hg(self, rc_web_server, tmpdir):
225 clone_url = rc_web_server.repo_clone_url(HG_REPO)
226 stdout, stderr = Command('/tmp').execute(
227 'hg clone', clone_url, tmpdir.strpath)
228
229 stdout, stderr = _add_files_and_push(
230 'hg', tmpdir.strpath, clone_url=clone_url)
231
232 assert 'pushing to' in stdout
233 assert 'size summary' in stdout
234
235 def test_push_new_file_git(self, rc_web_server, tmpdir):
143 def test_push_new_file_git(self, rc_web_server, tmpdir):
236 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
144 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
237 stdout, stderr = Command('/tmp').execute(
145 stdout, stderr = Command('/tmp').execute(
@@ -243,58 +151,6 b' class TestVCSOperations(object):'
243
151
244 _check_proper_git_push(stdout, stderr)
152 _check_proper_git_push(stdout, stderr)
245
153
246 def test_push_invalidates_cache(self, rc_web_server, tmpdir):
247 hg_repo = Repository.get_by_repo_name(HG_REPO)
248
249 # init cache objects
250 CacheKey.delete_all_cache()
251
252 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=hg_repo.repo_id)
253
254 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key)
255
256 with inv_context_manager as invalidation_context:
257 # __enter__ will create and register cache objects
258 pass
259
260 cache_keys = hg_repo.cache_keys
261 assert cache_keys != []
262 old_ids = [x.cache_state_uid for x in cache_keys]
263
264 # clone to init cache
265 clone_url = rc_web_server.repo_clone_url(hg_repo.repo_name)
266 stdout, stderr = Command('/tmp').execute(
267 'hg clone', clone_url, tmpdir.strpath)
268
269 cache_keys = hg_repo.cache_keys
270 assert cache_keys != []
271 for key in cache_keys:
272 assert key.cache_active is True
273
274 # PUSH that should trigger invalidation cache
275 stdout, stderr = _add_files_and_push(
276 'hg', tmpdir.strpath, clone_url=clone_url, files_no=1)
277
278 # flush...
279 Session().commit()
280 hg_repo = Repository.get_by_repo_name(HG_REPO)
281 cache_keys = hg_repo.cache_keys
282 assert cache_keys != []
283 new_ids = [x.cache_state_uid for x in cache_keys]
284 assert new_ids != old_ids
285
286 def test_push_wrong_credentials_hg(self, rc_web_server, tmpdir):
287 clone_url = rc_web_server.repo_clone_url(HG_REPO)
288 stdout, stderr = Command('/tmp').execute(
289 'hg clone', clone_url, tmpdir.strpath)
290
291 push_url = rc_web_server.repo_clone_url(
292 HG_REPO, user='bad', passwd='name')
293 stdout, stderr = _add_files_and_push(
294 'hg', tmpdir.strpath, clone_url=push_url)
295
296 assert 'abort: authorization failed' in stderr
297
298 def test_push_wrong_credentials_git(self, rc_web_server, tmpdir):
154 def test_push_wrong_credentials_git(self, rc_web_server, tmpdir):
299 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
155 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
300 stdout, stderr = Command('/tmp').execute(
156 stdout, stderr = Command('/tmp').execute(
@@ -307,17 +163,6 b' class TestVCSOperations(object):'
307
163
308 assert 'fatal: Authentication failed' in stderr
164 assert 'fatal: Authentication failed' in stderr
309
165
310 def test_push_back_to_wrong_url_hg(self, rc_web_server, tmpdir):
311 clone_url = rc_web_server.repo_clone_url(HG_REPO)
312 stdout, stderr = Command('/tmp').execute(
313 'hg clone', clone_url, tmpdir.strpath)
314
315 stdout, stderr = _add_files_and_push(
316 'hg', tmpdir.strpath,
317 clone_url=rc_web_server.repo_clone_url('not-existing'))
318
319 assert 'HTTP Error 404: Not Found' in stderr
320
321 def test_push_back_to_wrong_url_git(self, rc_web_server, tmpdir):
166 def test_push_back_to_wrong_url_git(self, rc_web_server, tmpdir):
322 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
167 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
323 stdout, stderr = Command('/tmp').execute(
168 stdout, stderr = Command('/tmp').execute(
@@ -329,28 +174,6 b' class TestVCSOperations(object):'
329
174
330 assert 'not found' in stderr
175 assert 'not found' in stderr
331
176
332 def test_ip_restriction_hg(self, rc_web_server, tmpdir):
333 user_model = UserModel()
334 try:
335 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
336 Session().commit()
337 time.sleep(2)
338 clone_url = rc_web_server.repo_clone_url(HG_REPO)
339 stdout, stderr = Command('/tmp').execute(
340 'hg clone', clone_url, tmpdir.strpath)
341 assert 'abort: HTTP Error 403: Forbidden' in stderr
342 finally:
343 # release IP restrictions
344 for ip in UserIpMap.getAll():
345 UserIpMap.delete(ip.ip_id)
346 Session().commit()
347
348 time.sleep(2)
349
350 stdout, stderr = Command('/tmp').execute(
351 'hg clone', clone_url, tmpdir.strpath)
352 _check_proper_clone(stdout, stderr, 'hg')
353
354 def test_ip_restriction_git(self, rc_web_server, tmpdir):
177 def test_ip_restriction_git(self, rc_web_server, tmpdir):
355 user_model = UserModel()
178 user_model = UserModel()
356 try:
179 try:
@@ -42,6 +42,7 b' connection_available = pytest.mark.skipi'
42 "enable_webhook_push_integration")
42 "enable_webhook_push_integration")
43 class TestVCSOperationsOnCustomIniConfig(object):
43 class TestVCSOperationsOnCustomIniConfig(object):
44
44
45 @connection_available
45 def test_push_tag_with_commit_hg(self, rc_web_server, tmpdir):
46 def test_push_tag_with_commit_hg(self, rc_web_server, tmpdir):
46 clone_url = rc_web_server.repo_clone_url(HG_REPO)
47 clone_url = rc_web_server.repo_clone_url(HG_REPO)
47 stdout, stderr = Command('/tmp').execute(
48 stdout, stderr = Command('/tmp').execute(
@@ -56,6 +57,7 b' class TestVCSOperationsOnCustomIniConfig'
56 assert 'ERROR' not in rc_log
57 assert 'ERROR' not in rc_log
57 assert "{'name': 'v1.0.0'," in rc_log
58 assert "{'name': 'v1.0.0'," in rc_log
58
59
60 @connection_available
59 def test_push_tag_with_commit_git(
61 def test_push_tag_with_commit_git(
60 self, rc_web_server, tmpdir):
62 self, rc_web_server, tmpdir):
61 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
63 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
@@ -71,6 +73,7 b' class TestVCSOperationsOnCustomIniConfig'
71 assert 'ERROR' not in rc_log
73 assert 'ERROR' not in rc_log
72 assert "{'name': 'v1.0.0'," in rc_log
74 assert "{'name': 'v1.0.0'," in rc_log
73
75
76 @connection_available
74 def test_push_tag_with_no_commit_git(
77 def test_push_tag_with_no_commit_git(
75 self, rc_web_server, tmpdir):
78 self, rc_web_server, tmpdir):
76 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
79 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
@@ -7,7 +7,7 b''
7 [server:main]
7 [server:main]
8 ; COMMON HOST/IP CONFIG
8 ; COMMON HOST/IP CONFIG
9 host = 127.0.0.1
9 host = 127.0.0.1
10 port = 9900
10 port = 10010
11
11
12
12
13 ; ###########################
13 ; ###########################
@@ -22,6 +22,17 b' use = egg:gunicorn#main'
22 [app:main]
22 [app:main]
23 ; The %(here)s variable will be replaced with the absolute path of parent directory
23 ; The %(here)s variable will be replaced with the absolute path of parent directory
24 ; of this file
24 ; of this file
25 ; Each option in the app:main can be override by an environmental variable
26 ;
27 ;To override an option:
28 ;
29 ;RC_<KeyName>
30 ;Everything should be uppercase, . and - should be replaced by _.
31 ;For example, if you have these configuration settings:
32 ;rc_cache.repo_object.backend = foo
33 ;can be overridden by
34 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
35
25 use = egg:rhodecode-vcsserver
36 use = egg:rhodecode-vcsserver
26
37
27 ; Pyramid default locales, we need this to be set
38 ; Pyramid default locales, we need this to be set
@@ -30,10 +41,14 b' pyramid.default_locale_name = en'
30 ; default locale used by VCS systems
41 ; default locale used by VCS systems
31 locale = en_US.UTF-8
42 locale = en_US.UTF-8
32
43
33 ; path to binaries for vcsserver, it should be set by the installer
44 ; path to binaries (hg,git,svn) for vcsserver, it should be set by the installer
34 ; at installation time, e.g /home/user/vcsserver-1/profile/bin
45 ; at installation time, e.g /home/user/.rccontrol/vcsserver-1/profile/bin
35 ; it can also be a path to nix-build output in case of development
46 ; or /usr/local/bin/rhodecode_bin/vcs_bin
36 core.binary_dir = ""
47 core.binary_dir =
48
49 ; Redis connection settings for svn integrations logic
50 ; This connection string needs to be the same on ce and vcsserver
51 vcs.svn.redis_conn = redis://redis:6379/0
37
52
38 ; Custom exception store path, defaults to TMPDIR
53 ; Custom exception store path, defaults to TMPDIR
39 ; This is used to store exception from RhodeCode in shared directory
54 ; This is used to store exception from RhodeCode in shared directory
@@ -52,14 +67,14 b' cache_dir = %(here)s/data'
52 ; ***************************************
67 ; ***************************************
53
68
54 ; `repo_object` cache settings for vcs methods for repositories
69 ; `repo_object` cache settings for vcs methods for repositories
55 rc_cache.repo_object.backend = dogpile.cache.rc.memory_lru
70 #rc_cache.repo_object.backend = dogpile.cache.rc.file_namespace
56
71
57 ; cache auto-expires after N seconds
72 ; cache auto-expires after N seconds
58 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
73 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
59 rc_cache.repo_object.expiration_time = 2592000
74 #rc_cache.repo_object.expiration_time = 2592000
60
75
61 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
76 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
62 #rc_cache.repo_object.arguments.filename = /tmp/vcsserver_cache.db
77 #rc_cache.repo_object.arguments.filename = /tmp/vcsserver_cache_repo_object.db
63
78
64 ; ***********************************************************
79 ; ***********************************************************
65 ; `repo_object` cache with redis backend
80 ; `repo_object` cache with redis backend
@@ -83,19 +98,32 b' rc_cache.repo_object.expiration_time = 2'
83 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
98 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
84 #rc_cache.repo_object.arguments.distributed_lock = true
99 #rc_cache.repo_object.arguments.distributed_lock = true
85
100
86 # legacy cache regions, please don't change
101 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
87 beaker.cache.regions = repo_object
102 #rc_cache.repo_object.arguments.lock_auto_renewal = true
88 beaker.cache.repo_object.type = memorylru
103
89 beaker.cache.repo_object.max_items = 100
104 ; Statsd client config, this is used to send metrics to statsd
90 # cache auto-expires after N seconds
105 ; We recommend setting statsd_exported and scrape them using Promethues
91 beaker.cache.repo_object.expire = 300
106 #statsd.enabled = false
92 beaker.cache.repo_object.enabled = true
107 #statsd.statsd_host = 0.0.0.0
108 #statsd.statsd_port = 8125
109 #statsd.statsd_prefix =
110 #statsd.statsd_ipv6 = false
93
111
112 ; configure logging automatically at server startup set to false
113 ; to use the below custom logging config.
114 ; RC_LOGGING_FORMATTER
115 ; RC_LOGGING_LEVEL
116 ; env variables can control the settings for logging in case of autoconfigure
94
117
118 #logging.autoconfigure = true
119
120 ; specify your own custom logging config file to configure logging
121 #logging.logging_conf_file = /path/to/custom_logging.ini
95
122
96 ; #####################
123 ; #####################
97 ; LOGGING CONFIGURATION
124 ; LOGGING CONFIGURATION
98 ; #####################
125 ; #####################
126
99 [loggers]
127 [loggers]
100 keys = root, vcsserver
128 keys = root, vcsserver
101
129
@@ -103,7 +131,7 b' keys = root, vcsserver'
103 keys = console
131 keys = console
104
132
105 [formatters]
133 [formatters]
106 keys = generic
134 keys = generic, json
107
135
108 ; #######
136 ; #######
109 ; LOGGERS
137 ; LOGGERS
@@ -113,12 +141,11 b' level = NOTSET'
113 handlers = console
141 handlers = console
114
142
115 [logger_vcsserver]
143 [logger_vcsserver]
116 level = DEBUG
144 level = INFO
117 handlers =
145 handlers =
118 qualname = vcsserver
146 qualname = vcsserver
119 propagate = 1
147 propagate = 1
120
148
121
122 ; ########
149 ; ########
123 ; HANDLERS
150 ; HANDLERS
124 ; ########
151 ; ########
@@ -127,6 +154,8 b' propagate = 1'
127 class = StreamHandler
154 class = StreamHandler
128 args = (sys.stderr, )
155 args = (sys.stderr, )
129 level = DEBUG
156 level = DEBUG
157 ; To enable JSON formatted logs replace 'generic' with 'json'
158 ; This allows sending properly formatted logs to grafana loki or elasticsearch
130 formatter = generic
159 formatter = generic
131
160
132 ; ##########
161 ; ##########
@@ -136,3 +165,7 b' formatter = generic'
136 [formatter_generic]
165 [formatter_generic]
137 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
166 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
138 datefmt = %Y-%m-%d %H:%M:%S
167 datefmt = %Y-%m-%d %H:%M:%S
168
169 [formatter_json]
170 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
171 class = vcsserver.lib._vendor.jsonlogger.JsonFormatter
@@ -176,18 +176,11 b' setup('
176 ('public/**', 'ignore', None),
176 ('public/**', 'ignore', None),
177 ]
177 ]
178 },
178 },
179 paster_plugins=['PasteScript'],
179
180 entry_points={
180 entry_points={
181 'paste.app_factory': [
181 'paste.app_factory': [
182 'main=rhodecode.config.middleware:make_pyramid_app',
182 'main=rhodecode.config.middleware:make_pyramid_app',
183 ],
183 ],
184 'paste.global_paster_command': [
185 'ishell=rhodecode.lib.paster_commands.ishell:Command',
186 'upgrade-db=rhodecode.lib.paster_commands.upgrade_db:UpgradeDb',
187
188 'setup-rhodecode=rhodecode.lib.paster_commands.deprecated.setup_rhodecode:Command',
189 'celeryd=rhodecode.lib.paster_commands.deprecated.celeryd:Command',
190 ],
191 'pyramid.pshell_runner': [
184 'pyramid.pshell_runner': [
192 'ipython = rhodecode.lib.pyramid_shell:ipython_shell_runner',
185 'ipython = rhodecode.lib.pyramid_shell:ipython_shell_runner',
193 ],
186 ],
@@ -196,7 +189,8 b' setup('
196 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main',
189 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main',
197 'rc-ishell=rhodecode.lib.rc_commands.ishell:main',
190 'rc-ishell=rhodecode.lib.rc_commands.ishell:main',
198 'rc-add-artifact=rhodecode.lib.rc_commands.add_artifact:main',
191 'rc-add-artifact=rhodecode.lib.rc_commands.add_artifact:main',
199 'rc-ssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper:main',
192 'rc-ssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper_v1:main',
193 'rc-ssh-wrapper-v2=rhodecode.apps.ssh_support.lib.ssh_wrapper_v2:main',
200 ],
194 ],
201 'beaker.backends': [
195 'beaker.backends': [
202 'memorylru_base=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase',
196 'memorylru_base=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase',
@@ -1,451 +0,0 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import time
21 import logging
22 import tempfile
23 import traceback
24 import threading
25 import socket
26 import msgpack
27 import gevent
28
29 from http.server import BaseHTTPRequestHandler
30 from socketserver import TCPServer
31
32 import rhodecode
33 from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected
34 from rhodecode.model import meta
35 from rhodecode.lib import hooks_base
36 from rhodecode.lib.utils2 import AttributeDict
37 from rhodecode.lib.pyramid_utils import get_config
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib import rc_cache
40
41 log = logging.getLogger(__name__)
42
43
44 class HooksHttpHandler(BaseHTTPRequestHandler):
45
46 JSON_HOOKS_PROTO = 'json.v1'
47 MSGPACK_HOOKS_PROTO = 'msgpack.v1'
48 # starting with RhodeCode 5.0.0 MsgPack is the default, prior it used json
49 DEFAULT_HOOKS_PROTO = MSGPACK_HOOKS_PROTO
50
51 @classmethod
52 def serialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO):
53 if proto == cls.MSGPACK_HOOKS_PROTO:
54 return msgpack.packb(data)
55 return json.dumps(data)
56
57 @classmethod
58 def deserialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO):
59 if proto == cls.MSGPACK_HOOKS_PROTO:
60 return msgpack.unpackb(data)
61 return json.loads(data)
62
63 def do_POST(self):
64 hooks_proto, method, extras = self._read_request()
65 log.debug('Handling HooksHttpHandler %s with %s proto', method, hooks_proto)
66
67 txn_id = getattr(self.server, 'txn_id', None)
68 if txn_id:
69 log.debug('Computing TXN_ID based on `%s`:`%s`',
70 extras['repository'], extras['txn_id'])
71 computed_txn_id = rc_cache.utils.compute_key_from_params(
72 extras['repository'], extras['txn_id'])
73 if txn_id != computed_txn_id:
74 raise Exception(
75 'TXN ID fail: expected {} got {} instead'.format(
76 txn_id, computed_txn_id))
77
78 request = getattr(self.server, 'request', None)
79 try:
80 hooks = Hooks(request=request, log_prefix='HOOKS: {} '.format(self.server.server_address))
81 result = self._call_hook_method(hooks, method, extras)
82
83 except Exception as e:
84 exc_tb = traceback.format_exc()
85 result = {
86 'exception': e.__class__.__name__,
87 'exception_traceback': exc_tb,
88 'exception_args': e.args
89 }
90 self._write_response(hooks_proto, result)
91
92 def _read_request(self):
93 length = int(self.headers['Content-Length'])
94 # respect sent headers, fallback to OLD proto for compatability
95 hooks_proto = self.headers.get('rc-hooks-protocol') or self.JSON_HOOKS_PROTO
96 if hooks_proto == self.MSGPACK_HOOKS_PROTO:
97 # support for new vcsserver msgpack based protocol hooks
98 body = self.rfile.read(length)
99 data = self.deserialize_data(body)
100 else:
101 body = self.rfile.read(length)
102 data = self.deserialize_data(body)
103
104 return hooks_proto, data['method'], data['extras']
105
106 def _write_response(self, hooks_proto, result):
107 self.send_response(200)
108 if hooks_proto == self.MSGPACK_HOOKS_PROTO:
109 self.send_header("Content-type", "application/msgpack")
110 self.end_headers()
111 data = self.serialize_data(result)
112 self.wfile.write(data)
113 else:
114 self.send_header("Content-type", "text/json")
115 self.end_headers()
116 data = self.serialize_data(result)
117 self.wfile.write(data)
118
119 def _call_hook_method(self, hooks, method, extras):
120 try:
121 result = getattr(hooks, method)(extras)
122 finally:
123 meta.Session.remove()
124 return result
125
126 def log_message(self, format, *args):
127 """
128 This is an overridden method of BaseHTTPRequestHandler which logs using
129 logging library instead of writing directly to stderr.
130 """
131
132 message = format % args
133
134 log.debug(
135 "HOOKS: client=%s - - [%s] %s", self.client_address,
136 self.log_date_time_string(), message)
137
138
139 class BaseHooksCallbackDaemon:
140 """
141 Basic context manager for actions that don't require some extra
142 """
143 def __init__(self):
144 self.hooks_module = Hooks.__module__
145
146 def __enter__(self):
147 log.debug('Running `%s` callback daemon', self.__class__.__name__)
148 return self
149
150 def __exit__(self, exc_type, exc_val, exc_tb):
151 log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
152
153
154 class CeleryHooksCallbackDaemon(BaseHooksCallbackDaemon):
155 """
156 Context manger for achieving a compatibility with celery backend
157 """
158
159 def __init__(self, config):
160 self.task_queue = config.get('app:main', 'celery.broker_url')
161 self.task_backend = config.get('app:main', 'celery.result_backend')
162
163
164 class ThreadedHookCallbackDaemon(object):
165
166 _callback_thread = None
167 _daemon = None
168 _done = False
169 use_gevent = False
170
171 def __init__(self, txn_id=None, host=None, port=None):
172 self._prepare(txn_id=txn_id, host=host, port=port)
173 if self.use_gevent:
174 self._run_func = self._run_gevent
175 self._stop_func = self._stop_gevent
176 else:
177 self._run_func = self._run
178 self._stop_func = self._stop
179
180 def __enter__(self):
181 log.debug('Running `%s` callback daemon', self.__class__.__name__)
182 self._run_func()
183 return self
184
185 def __exit__(self, exc_type, exc_val, exc_tb):
186 log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
187 self._stop_func()
188
189 def _prepare(self, txn_id=None, host=None, port=None):
190 raise NotImplementedError()
191
192 def _run(self):
193 raise NotImplementedError()
194
195 def _stop(self):
196 raise NotImplementedError()
197
198 def _run_gevent(self):
199 raise NotImplementedError()
200
201 def _stop_gevent(self):
202 raise NotImplementedError()
203
204
205 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
206 """
207 Context manager which will run a callback daemon in a background thread.
208 """
209
210 hooks_uri = None
211
212 # From Python docs: Polling reduces our responsiveness to a shutdown
213 # request and wastes cpu at all other times.
214 POLL_INTERVAL = 0.01
215
216 use_gevent = False
217
218 @property
219 def _hook_prefix(self):
220 return 'HOOKS: {} '.format(self.hooks_uri)
221
222 def get_hostname(self):
223 return socket.gethostname() or '127.0.0.1'
224
225 def get_available_port(self, min_port=20000, max_port=65535):
226 from rhodecode.lib.utils2 import get_available_port as _get_port
227 return _get_port(min_port, max_port)
228
229 def _prepare(self, txn_id=None, host=None, port=None):
230 from pyramid.threadlocal import get_current_request
231
232 if not host or host == "*":
233 host = self.get_hostname()
234 if not port:
235 port = self.get_available_port()
236
237 server_address = (host, port)
238 self.hooks_uri = '{}:{}'.format(host, port)
239 self.txn_id = txn_id
240 self._done = False
241
242 log.debug(
243 "%s Preparing HTTP callback daemon registering hook object: %s",
244 self._hook_prefix, HooksHttpHandler)
245
246 self._daemon = TCPServer(server_address, HooksHttpHandler)
247 # inject transaction_id for later verification
248 self._daemon.txn_id = self.txn_id
249
250 # pass the WEB app request into daemon
251 self._daemon.request = get_current_request()
252
253 def _run(self):
254 log.debug("Running thread-based loop of callback daemon in background")
255 callback_thread = threading.Thread(
256 target=self._daemon.serve_forever,
257 kwargs={'poll_interval': self.POLL_INTERVAL})
258 callback_thread.daemon = True
259 callback_thread.start()
260 self._callback_thread = callback_thread
261
262 def _run_gevent(self):
263 log.debug("Running gevent-based loop of callback daemon in background")
264 # create a new greenlet for the daemon's serve_forever method
265 callback_greenlet = gevent.spawn(
266 self._daemon.serve_forever,
267 poll_interval=self.POLL_INTERVAL)
268
269 # store reference to greenlet
270 self._callback_greenlet = callback_greenlet
271
272 # switch to this greenlet
273 gevent.sleep(0.01)
274
275 def _stop(self):
276 log.debug("Waiting for background thread to finish.")
277 self._daemon.shutdown()
278 self._callback_thread.join()
279 self._daemon = None
280 self._callback_thread = None
281 if self.txn_id:
282 txn_id_file = get_txn_id_data_path(self.txn_id)
283 log.debug('Cleaning up TXN ID %s', txn_id_file)
284 if os.path.isfile(txn_id_file):
285 os.remove(txn_id_file)
286
287 log.debug("Background thread done.")
288
289 def _stop_gevent(self):
290 log.debug("Waiting for background greenlet to finish.")
291
292 # if greenlet exists and is running
293 if self._callback_greenlet and not self._callback_greenlet.dead:
294 # shutdown daemon if it exists
295 if self._daemon:
296 self._daemon.shutdown()
297
298 # kill the greenlet
299 self._callback_greenlet.kill()
300
301 self._daemon = None
302 self._callback_greenlet = None
303
304 if self.txn_id:
305 txn_id_file = get_txn_id_data_path(self.txn_id)
306 log.debug('Cleaning up TXN ID %s', txn_id_file)
307 if os.path.isfile(txn_id_file):
308 os.remove(txn_id_file)
309
310 log.debug("Background greenlet done.")
311
312
313 def get_txn_id_data_path(txn_id):
314 import rhodecode
315
316 root = rhodecode.CONFIG.get('cache_dir') or tempfile.gettempdir()
317 final_dir = os.path.join(root, 'svn_txn_id')
318
319 if not os.path.isdir(final_dir):
320 os.makedirs(final_dir)
321 return os.path.join(final_dir, 'rc_txn_id_{}'.format(txn_id))
322
323
324 def store_txn_id_data(txn_id, data_dict):
325 if not txn_id:
326 log.warning('Cannot store txn_id because it is empty')
327 return
328
329 path = get_txn_id_data_path(txn_id)
330 try:
331 with open(path, 'wb') as f:
332 f.write(json.dumps(data_dict))
333 except Exception:
334 log.exception('Failed to write txn_id metadata')
335
336
337 def get_txn_id_from_store(txn_id):
338 """
339 Reads txn_id from store and if present returns the data for callback manager
340 """
341 path = get_txn_id_data_path(txn_id)
342 try:
343 with open(path, 'rb') as f:
344 return json.loads(f.read())
345 except Exception:
346 return {}
347
348
349 def prepare_callback_daemon(extras, protocol, host, txn_id=None):
350 txn_details = get_txn_id_from_store(txn_id)
351 port = txn_details.get('port', 0)
352 match protocol:
353 case 'http':
354 callback_daemon = HttpHooksCallbackDaemon(
355 txn_id=txn_id, host=host, port=port)
356 case 'celery':
357 callback_daemon = CeleryHooksCallbackDaemon(get_config(extras['config']))
358 case 'local':
359 callback_daemon = BaseHooksCallbackDaemon()
360 case _:
361 log.error('Unsupported callback daemon protocol "%s"', protocol)
362 raise Exception('Unsupported callback daemon protocol.')
363
364 extras['hooks_uri'] = getattr(callback_daemon, 'hooks_uri', '')
365 extras['task_queue'] = getattr(callback_daemon, 'task_queue', '')
366 extras['task_backend'] = getattr(callback_daemon, 'task_backend', '')
367 extras['hooks_protocol'] = protocol
368 extras['time'] = time.time()
369
370 # register txn_id
371 extras['txn_id'] = txn_id
372 log.debug('Prepared a callback daemon: %s',
373 callback_daemon.__class__.__name__)
374 return callback_daemon, extras
375
376
377 class Hooks(object):
378 """
379 Exposes the hooks for remote call backs
380 """
381 def __init__(self, request=None, log_prefix=''):
382 self.log_prefix = log_prefix
383 self.request = request
384
385 def repo_size(self, extras):
386 log.debug("%sCalled repo_size of %s object", self.log_prefix, self)
387 return self._call_hook(hooks_base.repo_size, extras)
388
389 def pre_pull(self, extras):
390 log.debug("%sCalled pre_pull of %s object", self.log_prefix, self)
391 return self._call_hook(hooks_base.pre_pull, extras)
392
393 def post_pull(self, extras):
394 log.debug("%sCalled post_pull of %s object", self.log_prefix, self)
395 return self._call_hook(hooks_base.post_pull, extras)
396
397 def pre_push(self, extras):
398 log.debug("%sCalled pre_push of %s object", self.log_prefix, self)
399 return self._call_hook(hooks_base.pre_push, extras)
400
401 def post_push(self, extras):
402 log.debug("%sCalled post_push of %s object", self.log_prefix, self)
403 return self._call_hook(hooks_base.post_push, extras)
404
405 def _call_hook(self, hook, extras):
406 extras = AttributeDict(extras)
407 server_url = extras['server_url']
408
409 extras.request = self.request
410
411 try:
412 result = hook(extras)
413 if result is None:
414 raise Exception(
415 'Failed to obtain hook result from func: {}'.format(hook))
416 except HTTPBranchProtected as handled_error:
417 # Those special cases doesn't need error reporting. It's a case of
418 # locked repo or protected branch
419 result = AttributeDict({
420 'status': handled_error.code,
421 'output': handled_error.explanation
422 })
423 except (HTTPLockedRC, Exception) as error:
424 # locked needs different handling since we need to also
425 # handle PULL operations
426 exc_tb = ''
427 if not isinstance(error, HTTPLockedRC):
428 exc_tb = traceback.format_exc()
429 log.exception('%sException when handling hook %s', self.log_prefix, hook)
430 error_args = error.args
431 return {
432 'status': 128,
433 'output': '',
434 'exception': type(error).__name__,
435 'exception_traceback': exc_tb,
436 'exception_args': error_args,
437 }
438 finally:
439 meta.Session.remove()
440
441 log.debug('%sGot hook call response %s', self.log_prefix, result)
442 return {
443 'status': result.status,
444 'output': result.output,
445 }
446
447 def __enter__(self):
448 return self
449
450 def __exit__(self, exc_type, exc_val, exc_tb):
451 pass
@@ -1,89 +0,0 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 import os
20 import logging
21
22 from paste.script.command import Command, BadCommand
23
24
25 class BasePasterCommand(Command):
26 """
27 Abstract Base Class for paster commands.
28
29 The celery commands are somewhat aggressive about loading
30 celery.conf, and since our module sets the `CELERY_LOADER`
31 environment variable to our loader, we have to bootstrap a bit and
32 make sure we've had a chance to load the pylons config off of the
33 command line, otherwise everything fails.
34 """
35 min_args = 1
36 min_args_error = "Please provide a paster config file as an argument."
37 takes_config_file = 1
38 requires_config_file = True
39
40 def notify_msg(self, msg, log=False):
41 """Make a notification to user, additionally if logger is passed
42 it logs this action using given logger
43
44 :param msg: message that will be printed to user
45 :param log: logging instance, to use to additionally log this message
46
47 """
48 if log and isinstance(log, logging):
49 log(msg)
50
51 def run(self, args):
52 """
53 Overrides Command.run
54
55 Checks for a config file argument and loads it.
56 """
57 if len(args) < self.min_args:
58 raise BadCommand(
59 self.min_args_error % {'min_args': self.min_args,
60 'actual_args': len(args)})
61
62 # Decrement because we're going to lob off the first argument.
63 # @@ This is hacky
64 self.min_args -= 1
65 self.bootstrap_config(args[0])
66 self.update_parser()
67 return super(BasePasterCommand, self).run(args[1:])
68
69 def update_parser(self):
70 """
71 Abstract method. Allows for the class' parser to be updated
72 before the superclass' `run` method is called. Necessary to
73 allow options/arguments to be passed through to the underlying
74 celery command.
75 """
76 raise NotImplementedError("Abstract Method.")
77
78 def bootstrap_config(self, conf):
79 """
80 Loads the pylons configuration.
81 """
82 self.path_to_ini_file = os.path.realpath(conf)
83
84 def _init_session(self):
85 """
86 Inits SqlAlchemy Session
87 """
88 logging.config.fileConfig(self.path_to_ini_file)
89
1 NO CONTENT: file was removed
NO CONTENT: file was removed
@@ -1,44 +0,0 b''
1
2
3 # Copyright (C) 2013-2023 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 from rhodecode.lib.paster_commands import BasePasterCommand
23
24
25 class Command(BasePasterCommand):
26 """
27 Start the celery worker
28
29 Starts the celery worker that uses a paste.deploy configuration
30 file.
31 """
32 usage = 'CONFIG_FILE'
33 summary = __doc__.splitlines()[0]
34 description = "".join(__doc__.splitlines()[2:])
35
36 parser = BasePasterCommand.standard_parser(quiet=True)
37
38 def update_parser(self):
39 pass
40
41 def command(self):
42 cmd = 'celery worker --task-events --beat --app rhodecode.lib.celerylib.loader --loglevel DEBUG --ini=%s' % self.path_to_ini_file
43 raise Exception('This Command is deprecated please run: %s' % cmd)
44
@@ -1,42 +0,0 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
20
21 from rhodecode.lib.paster_commands import BasePasterCommand
22
23
24 class Command(BasePasterCommand):
25 """
26 Start the celery worker
27
28 Starts the celery worker that uses a paste.deploy configuration
29 file.
30 """
31 usage = 'CONFIG_FILE [celeryd options...]'
32 summary = __doc__.splitlines()[0]
33 description = "".join(__doc__.splitlines()[2:])
34
35 parser = BasePasterCommand.standard_parser(quiet=True)
36
37 def update_parser(self):
38 pass
39
40 def command(self):
41 cmd = 'rc-setup-app %s' % self.path_to_ini_file
42 raise Exception('This Command is deprecated please run: %s' % cmd)
@@ -1,80 +0,0 b''
1
2
3 # Copyright (C) 2013-2023 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 interactive shell paster command for RhodeCode
23 """
24
25 import os
26 import sys
27 import logging
28
29 from rhodecode.lib.paster_commands import BasePasterCommand
30
31 # fix rhodecode import
32 from os.path import dirname as dn
33 rc_path = dn(dn(dn(os.path.realpath(__file__))))
34 sys.path.append(rc_path)
35
36 log = logging.getLogger(__name__)
37
38 welcome_banner = """Welcome to RhodeCode iShell.
39 Type `exit` to exit the shell.
40 iShell is interactive shell to interact directly with the
41 internal RhodeCode APIs. You can rescue your lost password,
42 or reset some user/system settings.
43 """
44
45
46 class Command(BasePasterCommand):
47
48 max_args = 1
49 min_args = 1
50
51 usage = "CONFIG_FILE"
52 group_name = "RhodeCode"
53 takes_config_file = -1
54 parser = BasePasterCommand.standard_parser(verbose=True)
55 summary = "Interactive shell"
56
57 def command(self):
58 #get SqlAlchemy session
59 self._init_session()
60
61 # imports, used in ipython shell
62 import os
63 import sys
64 import time
65 import shutil
66 import datetime
67 from rhodecode.model.db import *
68
69 try:
70 from IPython import embed
71 from traitlets.config import Config
72 cfg = Config()
73 cfg.InteractiveShellEmbed.confirm_exit = False
74 embed(config=cfg, banner1=welcome_banner)
75 except ImportError:
76 print('ipython installation required for ishell')
77 sys.exit(-1)
78
79 def update_parser(self):
80 pass
@@ -1,63 +0,0 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
20 import logging
21
22 from rhodecode.lib.paster_commands import BasePasterCommand, Command
23
24 log = logging.getLogger(__name__)
25
26
27 class UpgradeDb(BasePasterCommand):
28 """
29 Command used for paster to upgrade our database to newer version
30 """
31
32 max_args = 1
33 min_args = 1
34
35 usage = "CONFIG_FILE"
36 summary = "Upgrades current db to newer version"
37 group_name = "RhodeCode"
38
39 parser = Command.standard_parser(verbose=True)
40
41 def command(self):
42 from rhodecode.lib.rc_commands import upgrade_db
43 upgrade_db.command(
44 self.path_to_ini_file, self.options.__dict__.get('force_ask'), None)
45
46 def update_parser(self):
47 self.parser.add_option('--sql',
48 action='store_true',
49 dest='just_sql',
50 help="Prints upgrade sql for further investigation",
51 default=False)
52
53 self.parser.add_option('--force-yes',
54 action='store_true',
55 dest='force_ask',
56 default=None,
57 help='Force yes to every question')
58 self.parser.add_option('--force-no',
59 action='store_false',
60 dest='force_ask',
61 default=None,
62 help='Force no to every question')
63
General Comments 0
You need to be logged in to leave comments. Login now