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 | ; SSH calls. Set this for events to receive proper url for SSH calls. |
|
153 | ; SSH calls. Set this for events to receive proper url for SSH calls. | |
154 | app.base_url = http://rhodecode.local |
|
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 | ; Unique application ID. Should be a random unique string for security. |
|
162 | ; Unique application ID. Should be a random unique string for security. | |
157 | app_instance_uuid = rc-production |
|
163 | app_instance_uuid = rc-production | |
158 |
|
164 |
@@ -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 |
@@ -46,6 +46,7 b' log = logging.getLogger(__name__)' | |||||
46 |
|
46 | |||
47 | DEFAULT_RENDERER = 'jsonrpc_renderer' |
|
47 | DEFAULT_RENDERER = 'jsonrpc_renderer' | |
48 | DEFAULT_URL = '/_admin/apiv2' |
|
48 | DEFAULT_URL = '/_admin/apiv2' | |
|
49 | SERVICE_API_IDENTIFIER = 'service_' | |||
49 |
|
50 | |||
50 |
|
51 | |||
51 | def find_methods(jsonrpc_methods, pattern): |
|
52 | def find_methods(jsonrpc_methods, pattern): | |
@@ -54,7 +55,9 b' def find_methods(jsonrpc_methods, patter' | |||||
54 | pattern = [pattern] |
|
55 | pattern = [pattern] | |
55 |
|
56 | |||
56 | for single_pattern in pattern: |
|
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 | if fnmatch.fnmatch(method_name, single_pattern): |
|
61 | if fnmatch.fnmatch(method_name, single_pattern): | |
59 | matches[method_name] = method |
|
62 | matches[method_name] = method | |
60 | return matches |
|
63 | return matches | |
@@ -190,43 +193,48 b' def request_view(request):' | |||||
190 | # check if we can find this session using api_key, get_by_auth_token |
|
193 | # check if we can find this session using api_key, get_by_auth_token | |
191 | # search not expired tokens only |
|
194 | # search not expired tokens only | |
192 | try: |
|
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: |
|
199 | if api_user is None: | |
196 | return jsonrpc_error( |
|
200 | return jsonrpc_error( | |
197 | request, retid=request.rpc_id, message='Invalid API KEY') |
|
201 | request, retid=request.rpc_id, message='Invalid API KEY') | |
198 |
|
202 | |||
199 | if not api_user.active: |
|
203 | if not api_user.active: | |
200 | return jsonrpc_error( |
|
204 | return jsonrpc_error( | |
201 | request, retid=request.rpc_id, |
|
205 | request, retid=request.rpc_id, | |
202 | message='Request from this user not allowed') |
|
206 | message='Request from this user not allowed') | |
203 |
|
207 | |||
204 | # check if we are allowed to use this IP |
|
208 | # check if we are allowed to use this IP | |
205 | auth_u = AuthUser( |
|
209 | auth_u = AuthUser( | |
206 | api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr) |
|
210 | api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr) | |
207 | if not auth_u.ip_allowed: |
|
211 | if not auth_u.ip_allowed: | |
208 | return jsonrpc_error( |
|
212 | return jsonrpc_error( | |
209 | request, retid=request.rpc_id, |
|
213 | request, retid=request.rpc_id, | |
210 | message='Request from IP:{} not allowed'.format( |
|
214 | message='Request from IP:{} not allowed'.format( | |
211 | request.rpc_ip_addr)) |
|
215 | request.rpc_ip_addr)) | |
212 | else: |
|
216 | else: | |
213 | log.info('Access for IP:%s allowed', request.rpc_ip_addr) |
|
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 |
|
223 | # now check if token is valid for API | |
216 | request.rpc_user = auth_u |
|
224 | auth_token = request.rpc_api_key | |
217 | request.environ['rc_auth_user_id'] = str(auth_u.user_id) |
|
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 |
|
229 | log.debug('Checking if API KEY is valid with proper role') | |
220 | auth_token = request.rpc_api_key |
|
230 | if invalid_token: | |
221 | token_match = api_user.authenticate_by_token( |
|
231 | return jsonrpc_error( | |
222 | auth_token, roles=[UserApiKeys.ROLE_API]) |
|
232 | request, retid=request.rpc_id, | |
223 | invalid_token = not token_match |
|
233 | message='API KEY invalid or, has bad role for an API call') | |
224 |
|
234 | else: | ||
225 | log.debug('Checking if API KEY is valid with proper role') |
|
235 | auth_u = 'service' | |
226 | if invalid_token: |
|
236 | if request.rpc_api_key != request.registry.settings['app.service_api.token']: | |
227 | return jsonrpc_error( |
|
237 | raise Exception("Provided service secret is not recognized!") | |
228 | request, retid=request.rpc_id, |
|
|||
229 | message='API KEY invalid or, has bad role for an API call') |
|
|||
230 |
|
238 | |||
231 | except Exception: |
|
239 | except Exception: | |
232 | log.exception('Error on API AUTH') |
|
240 | log.exception('Error on API AUTH') | |
@@ -290,7 +298,8 b' def request_view(request):' | |||||
290 | }) |
|
298 | }) | |
291 |
|
299 | |||
292 | # register some common functions for usage |
|
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 | statsd = request.registry.statsd |
|
304 | statsd = request.registry.statsd | |
296 |
|
305 |
@@ -23,6 +23,7 b' import datetime' | |||||
23 | import configparser |
|
23 | import configparser | |
24 | from sqlalchemy import Table |
|
24 | from sqlalchemy import Table | |
25 |
|
25 | |||
|
26 | from rhodecode.lib.utils import call_service_api | |||
26 | from rhodecode.lib.utils2 import AttributeDict |
|
27 | from rhodecode.lib.utils2 import AttributeDict | |
27 | from rhodecode.model.scm import ScmModel |
|
28 | from rhodecode.model.scm import ScmModel | |
28 |
|
29 | |||
@@ -261,3 +262,131 b' class SshWrapper(object):' | |||||
261 | exit_code = -1 |
|
262 | exit_code = -1 | |
262 |
|
263 | |||
263 | return exit_code |
|
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 | from rhodecode.lib.hooks_daemon import prepare_callback_daemon |
|
23 | from rhodecode.lib.hooks_daemon 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.lib.utils import call_service_api | |||
26 | from rhodecode.model.scm import ScmModel |
|
27 | from rhodecode.model.scm import ScmModel | |
27 |
|
28 | |||
28 | log = logging.getLogger(__name__) |
|
29 | log = logging.getLogger(__name__) | |
@@ -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,15 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 | """ | |
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 | def has_write_perm(self): |
|
69 | def has_write_perm(self): | |
60 | permission = self.user_permissions.get(self.repo_name) |
|
70 | permission = self.user_permissions.get(self.repo_name) | |
@@ -65,30 +75,31 b' class VcsServer(object):' | |||||
65 |
|
75 | |||
66 | def _check_permissions(self, action): |
|
76 | def _check_permissions(self, action): | |
67 | permission = self.user_permissions.get(self.repo_name) |
|
77 | permission = self.user_permissions.get(self.repo_name) | |
|
78 | user_info = f'{self.user["user_id"]}:{self.user["username"]}' | |||
68 | log.debug('permission for %s on %s are: %s', |
|
79 | log.debug('permission for %s on %s are: %s', | |
69 |
|
|
80 | user_info, self.repo_name, permission) | |
70 |
|
81 | |||
71 | if not permission: |
|
82 | if not permission: | |
72 | log.error('user `%s` permissions to repo:%s are empty. Forbidding access.', |
|
83 | log.error('user `%s` permissions to repo:%s are empty. Forbidding access.', | |
73 |
|
|
84 | user_info, self.repo_name) | |
74 | return -2 |
|
85 | return -2 | |
75 |
|
86 | |||
76 | if action == 'pull': |
|
87 | if action == 'pull': | |
77 | if permission in self.read_perms: |
|
88 | if permission in self.read_perms: | |
78 | log.info( |
|
89 | log.info( | |
79 | 'READ Permissions for User "%s" detected to repo "%s"!', |
|
90 | 'READ Permissions for User "%s" detected to repo "%s"!', | |
80 |
|
|
91 | user_info, self.repo_name) | |
81 | return 0 |
|
92 | return 0 | |
82 | else: |
|
93 | else: | |
83 | if permission in self.write_perms: |
|
94 | if permission in self.write_perms: | |
84 | log.info( |
|
95 | log.info( | |
85 | 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!', |
|
96 | 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!', | |
86 |
|
|
97 | user_info, self.repo_name) | |
87 | return 0 |
|
98 | return 0 | |
88 |
|
99 | |||
89 | log.error('Cannot properly fetch or verify user `%s` permissions. ' |
|
100 | log.error('Cannot properly fetch or verify user `%s` permissions. ' | |
90 | 'Permissions: %s, vcs action: %s', |
|
101 | 'Permissions: %s, vcs action: %s', | |
91 |
|
|
102 | user_info, permission, action) | |
92 | return -2 |
|
103 | return -2 | |
93 |
|
104 | |||
94 | def update_environment(self, action, extras=None): |
|
105 | def update_environment(self, action, extras=None): | |
@@ -134,9 +145,10 b' class VcsServer(object):' | |||||
134 | if exit_code: |
|
145 | if exit_code: | |
135 | return exit_code, False |
|
146 | return exit_code, False | |
136 |
|
147 | |||
137 |
req = self.env |
|
148 | req = self.env.get('request') | |
138 | server_url = req.host_url + req.script_name |
|
149 | if req: | |
139 | extras['server_url'] = server_url |
|
150 | server_url = req.host_url + req.script_name | |
|
151 | extras['server_url'] = server_url | |||
140 |
|
152 | |||
141 | log.debug('Using %s binaries from path %s', self.backend, self._path) |
|
153 | log.debug('Using %s binaries from path %s', self.backend, self._path) | |
142 | exit_code = self.tunnel.run(extras) |
|
154 | exit_code = self.tunnel.run(extras) | |
@@ -144,12 +156,13 b' class VcsServer(object):' | |||||
144 | return exit_code, action == "push" |
|
156 | return exit_code, action == "push" | |
145 |
|
157 | |||
146 | def run(self, tunnel_extras=None): |
|
158 | def run(self, tunnel_extras=None): | |
|
159 | self.hooks_protocol = self.config.get('app:main', 'vcs.hooks.protocol') | |||
147 | tunnel_extras = tunnel_extras or {} |
|
160 | tunnel_extras = tunnel_extras or {} | |
148 | extras = {} |
|
161 | extras = {} | |
149 | extras.update(tunnel_extras) |
|
162 | extras.update(tunnel_extras) | |
150 |
|
163 | |||
151 | callback_daemon, extras = prepare_callback_daemon( |
|
164 | callback_daemon, extras = prepare_callback_daemon( | |
152 |
extras, protocol= |
|
165 | extras, protocol=self.hooks_protocol, | |
153 | host=vcs_settings.HOOKS_HOST) |
|
166 | host=vcs_settings.HOOKS_HOST) | |
154 |
|
167 | |||
155 | with callback_daemon: |
|
168 | with callback_daemon: |
@@ -23,6 +23,7 b' import tempfile' | |||||
23 | import textwrap |
|
23 | import textwrap | |
24 | import collections |
|
24 | import collections | |
25 | from .base import VcsServer |
|
25 | from .base import VcsServer | |
|
26 | from rhodecode.lib.utils import call_service_api | |||
26 | from rhodecode.model.db import RhodeCodeUi |
|
27 | from rhodecode.model.db import RhodeCodeUi | |
27 | from rhodecode.model.settings import VcsSettingsModel |
|
28 | from rhodecode.model.settings import VcsSettingsModel | |
28 |
|
29 | |||
@@ -108,6 +109,14 b' class MercurialServer(VcsServer):' | |||||
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.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 | ui_sections = collections.defaultdict(list) |
|
120 | ui_sections = collections.defaultdict(list) | |
112 | ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None) |
|
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 |
|
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 | 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 |
@@ -34,6 +34,7 b' import tarfile' | |||||
34 | import warnings |
|
34 | import warnings | |
35 | from functools import wraps |
|
35 | from functools import wraps | |
36 | from os.path import join as jn |
|
36 | from os.path import join as jn | |
|
37 | from configparser import NoOptionError | |||
37 |
|
38 | |||
38 | import paste |
|
39 | import paste | |
39 | import pkg_resources |
|
40 | import pkg_resources | |
@@ -52,6 +53,9 b' from rhodecode.model import meta' | |||||
52 | from rhodecode.model.db import ( |
|
53 | from rhodecode.model.db import ( | |
53 | Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup) |
|
54 | Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup) | |
54 | from rhodecode.model.meta import Session |
|
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 | log = logging.getLogger(__name__) |
|
61 | log = logging.getLogger(__name__) | |
@@ -821,3 +825,27 b' def send_test_email(recipients, email_bo' | |||||
821 | email_body = email_body_plaintext = email_body |
|
825 | email_body = email_body_plaintext = email_body | |
822 | subject = f'SUBJECT FROM: {socket.gethostname()}' |
|
826 | subject = f'SUBJECT FROM: {socket.gethostname()}' | |
823 | tasks.send_email(recipients, subject, email_body_plaintext, email_body) |
|
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 | 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. |
@@ -110,6 +110,7 b' def ini_config(request, tmpdir_factory, ' | |||||
110 | 'vcs.scm_app_implementation': 'http', |
|
110 | 'vcs.scm_app_implementation': 'http', | |
111 | 'vcs.hooks.protocol': 'http', |
|
111 | 'vcs.hooks.protocol': 'http', | |
112 | 'vcs.hooks.host': '*', |
|
112 | 'vcs.hooks.host': '*', | |
|
113 | 'app.service_api.token': 'service_secret_token', | |||
113 | }}, |
|
114 | }}, | |
114 |
|
115 | |||
115 | {'handler_console': { |
|
116 | {'handler_console': { |
@@ -196,7 +196,8 b' setup(' | |||||
196 | 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main', |
|
196 | 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main', | |
197 | 'rc-ishell=rhodecode.lib.rc_commands.ishell:main', |
|
197 | 'rc-ishell=rhodecode.lib.rc_commands.ishell:main', | |
198 | 'rc-add-artifact=rhodecode.lib.rc_commands.add_artifact:main', |
|
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 | 'beaker.backends': [ |
|
202 | 'beaker.backends': [ | |
202 | 'memorylru_base=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase', |
|
203 | 'memorylru_base=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase', |
General Comments 0
You need to be logged in to leave comments.
Login now