Show More
@@ -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,72 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 os | |
|
20 | import sys | |
|
21 | import time | |
|
22 | import logging | |
|
23 | ||
|
24 | import click | |
|
25 | ||
|
26 | from pyramid.paster import setup_logging | |
|
27 | ||
|
28 | from rhodecode.lib.statsd_client import StatsdClient | |
|
29 | from .backends import SshWrapperStandalone | |
|
30 | from .ssh_wrapper_v1 import setup_custom_logging | |
|
31 | ||
|
32 | log = logging.getLogger(__name__) | |
|
33 | ||
|
34 | ||
|
35 | @click.command() | |
|
36 | @click.argument('ini_path', type=click.Path(exists=True)) | |
|
37 | @click.option( | |
|
38 | '--mode', '-m', required=False, default='auto', | |
|
39 | type=click.Choice(['auto', 'vcs', 'git', 'hg', 'svn', 'test']), | |
|
40 | help='mode of operation') | |
|
41 | @click.option('--user', help='Username for which the command will be executed') | |
|
42 | @click.option('--user-id', help='User ID for which the command will be executed') | |
|
43 | @click.option('--key-id', help='ID of the key from the database') | |
|
44 | @click.option('--shell', '-s', is_flag=True, help='Allow Shell') | |
|
45 | @click.option('--debug', is_flag=True, help='Enabled detailed output logging') | |
|
46 | def main(ini_path, mode, user, user_id, key_id, shell, debug): | |
|
47 | setup_custom_logging(ini_path, debug) | |
|
48 | ||
|
49 | command = os.environ.get('SSH_ORIGINAL_COMMAND', '') | |
|
50 | if not command and mode not in ['test']: | |
|
51 | raise ValueError( | |
|
52 | 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.' | |
|
53 | 'Please make sure this is set and available during execution ' | |
|
54 | 'of this script.') | |
|
55 | connection_info = os.environ.get('SSH_CONNECTION', '') | |
|
56 | time_start = time.time() | |
|
57 | env = {'RC_CMD_SSH_WRAPPER': '1'} | |
|
58 | statsd = StatsdClient.statsd | |
|
59 | try: | |
|
60 | ssh_wrapper = SshWrapperStandalone( | |
|
61 | command, connection_info, mode, | |
|
62 | user, user_id, key_id, shell, ini_path, env) | |
|
63 | except Exception: | |
|
64 | log.exception('Failed to execute SshWrapper') | |
|
65 | sys.exit(-5) | |
|
66 | return_code = ssh_wrapper.wrap() | |
|
67 | operation_took = time.time() - time_start | |
|
68 | if statsd: | |
|
69 | operation_took_ms = round(1000.0 * operation_took) | |
|
70 | statsd.timing("rhodecode_ssh_wrapper_timing.histogram", operation_took_ms, | |
|
71 | use_decimals=False) | |
|
72 | sys.exit(return_code) |
@@ -153,6 +153,12 b' startup.import_repos = false' | |||
|
153 | 153 | ; SSH calls. Set this for events to receive proper url for SSH calls. |
|
154 | 154 | app.base_url = http://rhodecode.local |
|
155 | 155 | |
|
156 | ; Host at which the Service API is running. | |
|
157 | app.service_api.host = http://rhodecode.local:10020 | |
|
158 | ||
|
159 | ; Secret for Service API authentication. | |
|
160 | app.service_api.token = | |
|
161 | ||
|
156 | 162 | ; Unique application ID. Should be a random unique string for security. |
|
157 | 163 | app_instance_uuid = rc-production |
|
158 | 164 |
@@ -104,6 +104,12 b' startup.import_repos = false' | |||
|
104 | 104 | ; SSH calls. Set this for events to receive proper url for SSH calls. |
|
105 | 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 | 113 | ; Unique application ID. Should be a random unique string for security. |
|
108 | 114 | app_instance_uuid = rc-production |
|
109 | 115 |
@@ -46,6 +46,7 b' log = logging.getLogger(__name__)' | |||
|
46 | 46 | |
|
47 | 47 | DEFAULT_RENDERER = 'jsonrpc_renderer' |
|
48 | 48 | DEFAULT_URL = '/_admin/apiv2' |
|
49 | SERVICE_API_IDENTIFIER = 'service_' | |
|
49 | 50 | |
|
50 | 51 | |
|
51 | 52 | def find_methods(jsonrpc_methods, pattern): |
@@ -54,7 +55,9 b' def find_methods(jsonrpc_methods, patter' | |||
|
54 | 55 | pattern = [pattern] |
|
55 | 56 | |
|
56 | 57 | for single_pattern in pattern: |
|
57 |
for method_name, method in |
|
|
58 | for method_name, method in filter( | |
|
59 | lambda x: not x[0].startswith(SERVICE_API_IDENTIFIER), jsonrpc_methods.items() | |
|
60 | ): | |
|
58 | 61 | if fnmatch.fnmatch(method_name, single_pattern): |
|
59 | 62 | matches[method_name] = method |
|
60 | 63 | return matches |
@@ -190,43 +193,48 b' def request_view(request):' | |||
|
190 | 193 | # check if we can find this session using api_key, get_by_auth_token |
|
191 | 194 | # search not expired tokens only |
|
192 | 195 | try: |
|
193 | api_user = User.get_by_auth_token(request.rpc_api_key) | |
|
196 | if not request.rpc_method.startswith(SERVICE_API_IDENTIFIER): | |
|
197 | api_user = User.get_by_auth_token(request.rpc_api_key) | |
|
194 | 198 | |
|
195 | if api_user is None: | |
|
196 | return jsonrpc_error( | |
|
197 | request, retid=request.rpc_id, message='Invalid API KEY') | |
|
199 | if api_user is None: | |
|
200 | return jsonrpc_error( | |
|
201 | request, retid=request.rpc_id, message='Invalid API KEY') | |
|
198 | 202 | |
|
199 | if not api_user.active: | |
|
200 | return jsonrpc_error( | |
|
201 | request, retid=request.rpc_id, | |
|
202 | message='Request from this user not allowed') | |
|
203 | if not api_user.active: | |
|
204 | return jsonrpc_error( | |
|
205 | request, retid=request.rpc_id, | |
|
206 | message='Request from this user not allowed') | |
|
203 | 207 | |
|
204 | # check if we are allowed to use this IP | |
|
205 | auth_u = AuthUser( | |
|
206 | api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr) | |
|
207 | if not auth_u.ip_allowed: | |
|
208 | return jsonrpc_error( | |
|
209 | request, retid=request.rpc_id, | |
|
210 | message='Request from IP:{} not allowed'.format( | |
|
211 | request.rpc_ip_addr)) | |
|
212 | else: | |
|
213 | log.info('Access for IP:%s allowed', request.rpc_ip_addr) | |
|
208 | # check if we are allowed to use this IP | |
|
209 | auth_u = AuthUser( | |
|
210 | api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr) | |
|
211 | if not auth_u.ip_allowed: | |
|
212 | return jsonrpc_error( | |
|
213 | request, retid=request.rpc_id, | |
|
214 | message='Request from IP:{} not allowed'.format( | |
|
215 | request.rpc_ip_addr)) | |
|
216 | else: | |
|
217 | log.info('Access for IP:%s allowed', request.rpc_ip_addr) | |
|
218 | ||
|
219 | # register our auth-user | |
|
220 | request.rpc_user = auth_u | |
|
221 | request.environ['rc_auth_user_id'] = str(auth_u.user_id) | |
|
214 | 222 | |
|
215 | # register our auth-user | |
|
216 | request.rpc_user = auth_u | |
|
217 | request.environ['rc_auth_user_id'] = str(auth_u.user_id) | |
|
223 | # now check if token is valid for API | |
|
224 | auth_token = request.rpc_api_key | |
|
225 | token_match = api_user.authenticate_by_token( | |
|
226 | auth_token, roles=[UserApiKeys.ROLE_API]) | |
|
227 | invalid_token = not token_match | |
|
218 | 228 | |
|
219 | # now check if token is valid for API | |
|
220 | auth_token = request.rpc_api_key | |
|
221 | token_match = api_user.authenticate_by_token( | |
|
222 | auth_token, roles=[UserApiKeys.ROLE_API]) | |
|
223 | invalid_token = not token_match | |
|
224 | ||
|
225 | log.debug('Checking if API KEY is valid with proper role') | |
|
226 | if invalid_token: | |
|
227 | return jsonrpc_error( | |
|
228 | request, retid=request.rpc_id, | |
|
229 | message='API KEY invalid or, has bad role for an API call') | |
|
229 | log.debug('Checking if API KEY is valid with proper role') | |
|
230 | if invalid_token: | |
|
231 | return jsonrpc_error( | |
|
232 | request, retid=request.rpc_id, | |
|
233 | message='API KEY invalid or, has bad role for an API call') | |
|
234 | else: | |
|
235 | auth_u = 'service' | |
|
236 | if request.rpc_api_key != request.registry.settings['app.service_api.token']: | |
|
237 | raise Exception("Provided service secret is not recognized!") | |
|
230 | 238 | |
|
231 | 239 | except Exception: |
|
232 | 240 | log.exception('Error on API AUTH') |
@@ -290,7 +298,8 b' def request_view(request):' | |||
|
290 | 298 | }) |
|
291 | 299 | |
|
292 | 300 | # register some common functions for usage |
|
293 | attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id) | |
|
301 | rpc_user = request.rpc_user.user_id if hasattr(request, 'rpc_user') else None | |
|
302 | attach_context_attributes(TemplateArgs(), request, rpc_user) | |
|
294 | 303 | |
|
295 | 304 | statsd = request.registry.statsd |
|
296 | 305 |
@@ -23,6 +23,7 b' import datetime' | |||
|
23 | 23 | import configparser |
|
24 | 24 | from sqlalchemy import Table |
|
25 | 25 | |
|
26 | from rhodecode.lib.utils import call_service_api | |
|
26 | 27 | from rhodecode.lib.utils2 import AttributeDict |
|
27 | 28 | from rhodecode.model.scm import ScmModel |
|
28 | 29 | |
@@ -261,3 +262,131 b' class SshWrapper(object):' | |||
|
261 | 262 | exit_code = -1 |
|
262 | 263 | |
|
263 | 264 | return exit_code |
|
265 | ||
|
266 | ||
|
267 | class SshWrapperStandalone(SshWrapper): | |
|
268 | """ | |
|
269 | New version of SshWrapper designed to be depended only on service API | |
|
270 | """ | |
|
271 | repos_path = None | |
|
272 | ||
|
273 | @staticmethod | |
|
274 | def parse_user_related_data(user_data): | |
|
275 | user = AttributeDict() | |
|
276 | user.user_id = user_data['user_id'] | |
|
277 | user.username = user_data['username'] | |
|
278 | user.repo_permissions = user_data['repo_permissions'] | |
|
279 | user.branch_permissions = user_data['branch_permissions'] | |
|
280 | return user | |
|
281 | ||
|
282 | def wrap(self): | |
|
283 | mode = self.mode | |
|
284 | username = self.username | |
|
285 | user_id = self.user_id | |
|
286 | shell = self.shell | |
|
287 | ||
|
288 | scm_detected, scm_repo, scm_mode = self.get_repo_details(mode) | |
|
289 | ||
|
290 | log.debug( | |
|
291 | 'Mode: `%s` User: `name:%s : id:%s` Shell: `%s` SSH Command: `\"%s\"` ' | |
|
292 | 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`', | |
|
293 | mode, username, user_id, shell, self.command, | |
|
294 | scm_detected, scm_mode, scm_repo) | |
|
295 | ||
|
296 | log.debug('SSH Connection info %s', self.get_connection_info()) | |
|
297 | ||
|
298 | if shell and self.command is None: | |
|
299 | log.info('Dropping to shell, no command given and shell is allowed') | |
|
300 | os.execl('/bin/bash', '-l') | |
|
301 | exit_code = 1 | |
|
302 | ||
|
303 | elif scm_detected: | |
|
304 | data = call_service_api(self.ini_path, { | |
|
305 | "method": "service_get_data_for_ssh_wrapper", | |
|
306 | "args": {"user_id": user_id, "repo_name": scm_repo, "key_id": self.key_id} | |
|
307 | }) | |
|
308 | user = self.parse_user_related_data(data) | |
|
309 | if not user: | |
|
310 | log.warning('User with id %s not found', user_id) | |
|
311 | exit_code = -1 | |
|
312 | return exit_code | |
|
313 | self.repos_path = data['repos_path'] | |
|
314 | permissions = user.repo_permissions | |
|
315 | repo_branch_permissions = user.branch_permissions | |
|
316 | try: | |
|
317 | exit_code, is_updated = self.serve( | |
|
318 | scm_detected, scm_repo, scm_mode, user, permissions, | |
|
319 | repo_branch_permissions) | |
|
320 | except Exception: | |
|
321 | log.exception('Error occurred during execution of SshWrapper') | |
|
322 | exit_code = -1 | |
|
323 | ||
|
324 | elif self.command is None and shell is False: | |
|
325 | log.error('No Command given.') | |
|
326 | exit_code = -1 | |
|
327 | ||
|
328 | else: | |
|
329 | log.error('Unhandled Command: "%s" Aborting.', self.command) | |
|
330 | exit_code = -1 | |
|
331 | ||
|
332 | return exit_code | |
|
333 | ||
|
334 | def maybe_translate_repo_uid(self, repo_name): | |
|
335 | _org_name = repo_name | |
|
336 | if _org_name.startswith('_'): | |
|
337 | _org_name = _org_name.split('/', 1)[0] | |
|
338 | ||
|
339 | if repo_name.startswith('_'): | |
|
340 | org_repo_name = repo_name | |
|
341 | log.debug('translating UID repo %s', org_repo_name) | |
|
342 | by_id_match = call_service_api(self.ini_path, { | |
|
343 | 'method': 'service_get_repo_name_by_id', | |
|
344 | "args": {"repo_id": repo_name} | |
|
345 | }) | |
|
346 | if by_id_match: | |
|
347 | repo_name = by_id_match['repo_name'] | |
|
348 | log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name) | |
|
349 | ||
|
350 | return repo_name, _org_name | |
|
351 | ||
|
352 | def serve(self, vcs, repo, mode, user, permissions, branch_permissions): | |
|
353 | store = self.repos_path | |
|
354 | ||
|
355 | check_branch_perms = False | |
|
356 | detect_force_push = False | |
|
357 | ||
|
358 | if branch_permissions: | |
|
359 | check_branch_perms = True | |
|
360 | detect_force_push = True | |
|
361 | ||
|
362 | log.debug( | |
|
363 | 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s', | |
|
364 | vcs, mode, repo, check_branch_perms) | |
|
365 | ||
|
366 | # detect if we have to check branch permissions | |
|
367 | extras = { | |
|
368 | 'detect_force_push': detect_force_push, | |
|
369 | 'check_branch_perms': check_branch_perms, | |
|
370 | 'config': self.ini_path | |
|
371 | } | |
|
372 | ||
|
373 | match vcs: | |
|
374 | case 'hg': | |
|
375 | server = MercurialServer( | |
|
376 | store=store, ini_path=self.ini_path, | |
|
377 | repo_name=repo, user=user, | |
|
378 | user_permissions=permissions, config=self.config, env=self.env) | |
|
379 | case 'git': | |
|
380 | server = GitServer( | |
|
381 | store=store, ini_path=self.ini_path, | |
|
382 | repo_name=repo, repo_mode=mode, user=user, | |
|
383 | user_permissions=permissions, config=self.config, env=self.env) | |
|
384 | case 'svn': | |
|
385 | server = SubversionServer( | |
|
386 | store=store, ini_path=self.ini_path, | |
|
387 | repo_name=None, user=user, | |
|
388 | user_permissions=permissions, config=self.config, env=self.env) | |
|
389 | case _: | |
|
390 | raise Exception(f'Unrecognised VCS: {vcs}') | |
|
391 | self.server_impl = server | |
|
392 | return server.run(tunnel_extras=extras) |
@@ -23,6 +23,7 b' import logging' | |||
|
23 | 23 | from rhodecode.lib.hooks_daemon import prepare_callback_daemon |
|
24 | 24 | from rhodecode.lib.ext_json import sjson as json |
|
25 | 25 | from rhodecode.lib.vcs.conf import settings as vcs_settings |
|
26 | from rhodecode.lib.utils import call_service_api | |
|
26 | 27 | from rhodecode.model.scm import ScmModel |
|
27 | 28 | |
|
28 | 29 | log = logging.getLogger(__name__) |
@@ -47,6 +48,7 b' class VcsServer(object):' | |||
|
47 | 48 | self.repo_mode = None |
|
48 | 49 | self.store = '' |
|
49 | 50 | self.ini_path = '' |
|
51 | self.hooks_protocol = None | |
|
50 | 52 | |
|
51 | 53 | def _invalidate_cache(self, repo_name): |
|
52 | 54 | """ |
@@ -54,7 +56,15 b' class VcsServer(object):' | |||
|
54 | 56 | |
|
55 | 57 | :param repo_name: full repo name, also a cache key |
|
56 | 58 | """ |
|
57 | ScmModel().mark_for_invalidation(repo_name) | |
|
59 | # Todo: Leave only "celery" case after transition. | |
|
60 | match self.hooks_protocol: | |
|
61 | case 'http': | |
|
62 | ScmModel().mark_for_invalidation(repo_name) | |
|
63 | case 'celery': | |
|
64 | call_service_api(self.ini_path, { | |
|
65 | "method": "service_mark_for_invalidation", | |
|
66 | "args": {"repo_name": repo_name} | |
|
67 | }) | |
|
58 | 68 | |
|
59 | 69 | def has_write_perm(self): |
|
60 | 70 | permission = self.user_permissions.get(self.repo_name) |
@@ -65,30 +75,31 b' class VcsServer(object):' | |||
|
65 | 75 | |
|
66 | 76 | def _check_permissions(self, action): |
|
67 | 77 | permission = self.user_permissions.get(self.repo_name) |
|
78 | user_info = f'{self.user["user_id"]}:{self.user["username"]}' | |
|
68 | 79 | log.debug('permission for %s on %s are: %s', |
|
69 |
|
|
|
80 | user_info, self.repo_name, permission) | |
|
70 | 81 | |
|
71 | 82 | if not permission: |
|
72 | 83 | log.error('user `%s` permissions to repo:%s are empty. Forbidding access.', |
|
73 |
|
|
|
84 | user_info, self.repo_name) | |
|
74 | 85 | return -2 |
|
75 | 86 | |
|
76 | 87 | if action == 'pull': |
|
77 | 88 | if permission in self.read_perms: |
|
78 | 89 | log.info( |
|
79 | 90 | 'READ Permissions for User "%s" detected to repo "%s"!', |
|
80 |
|
|
|
91 | user_info, self.repo_name) | |
|
81 | 92 | return 0 |
|
82 | 93 | else: |
|
83 | 94 | if permission in self.write_perms: |
|
84 | 95 | log.info( |
|
85 | 96 | 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!', |
|
86 |
|
|
|
97 | user_info, self.repo_name) | |
|
87 | 98 | return 0 |
|
88 | 99 | |
|
89 | 100 | log.error('Cannot properly fetch or verify user `%s` permissions. ' |
|
90 | 101 | 'Permissions: %s, vcs action: %s', |
|
91 |
|
|
|
102 | user_info, permission, action) | |
|
92 | 103 | return -2 |
|
93 | 104 | |
|
94 | 105 | def update_environment(self, action, extras=None): |
@@ -134,9 +145,10 b' class VcsServer(object):' | |||
|
134 | 145 | if exit_code: |
|
135 | 146 | return exit_code, False |
|
136 | 147 | |
|
137 |
req = self.env |
|
|
138 | server_url = req.host_url + req.script_name | |
|
139 | extras['server_url'] = server_url | |
|
148 | req = self.env.get('request') | |
|
149 | if req: | |
|
150 | server_url = req.host_url + req.script_name | |
|
151 | extras['server_url'] = server_url | |
|
140 | 152 | |
|
141 | 153 | log.debug('Using %s binaries from path %s', self.backend, self._path) |
|
142 | 154 | exit_code = self.tunnel.run(extras) |
@@ -144,12 +156,13 b' class VcsServer(object):' | |||
|
144 | 156 | return exit_code, action == "push" |
|
145 | 157 | |
|
146 | 158 | def run(self, tunnel_extras=None): |
|
159 | self.hooks_protocol = self.config.get('app:main', 'vcs.hooks.protocol') | |
|
147 | 160 | tunnel_extras = tunnel_extras or {} |
|
148 | 161 | extras = {} |
|
149 | 162 | extras.update(tunnel_extras) |
|
150 | 163 | |
|
151 | 164 | callback_daemon, extras = prepare_callback_daemon( |
|
152 |
extras, protocol= |
|
|
165 | extras, protocol=self.hooks_protocol, | |
|
153 | 166 | host=vcs_settings.HOOKS_HOST) |
|
154 | 167 | |
|
155 | 168 | with callback_daemon: |
@@ -23,6 +23,7 b' import tempfile' | |||
|
23 | 23 | import textwrap |
|
24 | 24 | import collections |
|
25 | 25 | from .base import VcsServer |
|
26 | from rhodecode.lib.utils import call_service_api | |
|
26 | 27 | from rhodecode.model.db import RhodeCodeUi |
|
27 | 28 | from rhodecode.model.settings import VcsSettingsModel |
|
28 | 29 | |
@@ -108,6 +109,14 b' class MercurialServer(VcsServer):' | |||
|
108 | 109 | self.tunnel = MercurialTunnelWrapper(server=self) |
|
109 | 110 | |
|
110 | 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.ini_path, { | |
|
115 | "method": "service_config_to_hgrc", | |
|
116 | "args": {"cli_flags": self.cli_flags, "repo_name": repo_name} | |
|
117 | }) | |
|
118 | return data['flags'] | |
|
119 | ||
|
111 | 120 | ui_sections = collections.defaultdict(list) |
|
112 | 121 | ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None) |
|
113 | 122 |
|
1 | NO CONTENT: file renamed from rhodecode/apps/ssh_support/lib/ssh_wrapper.py to rhodecode/apps/ssh_support/lib/ssh_wrapper_v1.py |
@@ -20,7 +20,7 b' import os' | |||
|
20 | 20 | import pytest |
|
21 | 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 | 24 | from rhodecode.lib.utils2 import AttributeDict |
|
25 | 25 | |
|
26 | 26 |
@@ -34,6 +34,7 b' import tarfile' | |||
|
34 | 34 | import warnings |
|
35 | 35 | from functools import wraps |
|
36 | 36 | from os.path import join as jn |
|
37 | from configparser import NoOptionError | |
|
37 | 38 | |
|
38 | 39 | import paste |
|
39 | 40 | import pkg_resources |
@@ -52,6 +53,9 b' from rhodecode.model import meta' | |||
|
52 | 53 | from rhodecode.model.db import ( |
|
53 | 54 | Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup) |
|
54 | 55 | from rhodecode.model.meta import Session |
|
56 | from rhodecode.lib.pyramid_utils import get_config | |
|
57 | from rhodecode.lib.vcs import CurlSession | |
|
58 | from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError | |
|
55 | 59 | |
|
56 | 60 | |
|
57 | 61 | log = logging.getLogger(__name__) |
@@ -821,3 +825,27 b' def send_test_email(recipients, email_bo' | |||
|
821 | 825 | email_body = email_body_plaintext = email_body |
|
822 | 826 | subject = f'SUBJECT FROM: {socket.gethostname()}' |
|
823 | 827 | tasks.send_email(recipients, subject, email_body_plaintext, email_body) |
|
828 | ||
|
829 | ||
|
830 | def call_service_api(ini_path, payload): | |
|
831 | config = get_config(ini_path) | |
|
832 | try: | |
|
833 | host = config.get('app:main', 'app.service_api.host') | |
|
834 | except NoOptionError: | |
|
835 | raise ImproperlyConfiguredError( | |
|
836 | "app.service_api.host is missing. " | |
|
837 | "Please ensure that app.service_api.host and app.service_api.token are " | |
|
838 | "defined inside of .ini configuration file." | |
|
839 | ) | |
|
840 | api_url = config.get('app:main', 'rhodecode.api.url') | |
|
841 | payload.update({ | |
|
842 | 'id': 'service', | |
|
843 | 'auth_token': config.get('app:main', 'app.service_api.token') | |
|
844 | }) | |
|
845 | ||
|
846 | response = CurlSession().post(f'{host}{api_url}', json.dumps(payload)) | |
|
847 | ||
|
848 | if response.status_code != 200: | |
|
849 | raise Exception("Service API responded with error") | |
|
850 | ||
|
851 | return json.loads(response.content)['result'] |
@@ -146,6 +146,10 b' class CommandError(VCSError):' | |||
|
146 | 146 | pass |
|
147 | 147 | |
|
148 | 148 | |
|
149 | class ImproperlyConfiguredError(Exception): | |
|
150 | pass | |
|
151 | ||
|
152 | ||
|
149 | 153 | class UnhandledException(VCSError): |
|
150 | 154 | """ |
|
151 | 155 | Signals that something unexpected went wrong. |
@@ -110,6 +110,7 b' def ini_config(request, tmpdir_factory, ' | |||
|
110 | 110 | 'vcs.scm_app_implementation': 'http', |
|
111 | 111 | 'vcs.hooks.protocol': 'http', |
|
112 | 112 | 'vcs.hooks.host': '*', |
|
113 | 'app.service_api.token': 'service_secret_token', | |
|
113 | 114 | }}, |
|
114 | 115 | |
|
115 | 116 | {'handler_console': { |
@@ -196,7 +196,8 b' setup(' | |||
|
196 | 196 | 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main', |
|
197 | 197 | 'rc-ishell=rhodecode.lib.rc_commands.ishell:main', |
|
198 | 198 | 'rc-add-artifact=rhodecode.lib.rc_commands.add_artifact:main', |
|
199 | 'rc-ssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper:main', | |
|
199 | 'rc-ssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper_v1:main', | |
|
200 | 'rc-ssh-wrapper-v2=rhodecode.apps.ssh_support.lib.ssh_wrapper_v2:main', | |
|
200 | 201 | ], |
|
201 | 202 | 'beaker.backends': [ |
|
202 | 203 | 'memorylru_base=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase', |
General Comments 0
You need to be logged in to leave comments.
Login now