##// END OF EJS Templates
refactor(ssh-wrapper): changed SSHVcsServer to SshVcsServer, updated call_service_api method.
ilin.s -
r5326:cede61e3 default
parent child Browse files
Show More
@@ -1,417 +1,387 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 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
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
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/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
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
19 import os
19 import os
20 import re
20 import re
21 import logging
21 import logging
22 import datetime
22 import datetime
23 from sqlalchemy import Table
23 from sqlalchemy import Table
24
24
25 from rhodecode.lib.api_utils import call_service_api
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.lib.vcs.exceptions import ImproperlyConfiguredError
28
27
29 from .hg import MercurialServer
28 from .hg import MercurialServer
30 from .git import GitServer
29 from .git import GitServer
31 from .svn import SubversionServer
30 from .svn import SubversionServer
32 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
33
32
34
33
35 class SshWrapper(object):
34 class SshWrapper(object):
36 hg_cmd_pat = re.compile(r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$')
35 hg_cmd_pat = re.compile(r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$')
37 git_cmd_pat = re.compile(r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
36 git_cmd_pat = re.compile(r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
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, settings, 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
45 self.username = user
44 self.username = user
46 self.user_id = user_id
45 self.user_id = user_id
47 self.key_id = key_id
46 self.key_id = key_id
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 self.settings = settings
50 self.settings = settings
52 self.server_impl = None
51 self.server_impl = None
53
52
54 def update_key_access_time(self, key_id):
53 def update_key_access_time(self, key_id):
55 from rhodecode.model.meta import raw_query_executor, Base
54 from rhodecode.model.meta import raw_query_executor, Base
56
55
57 table = Table('user_ssh_keys', Base.metadata, autoload=False)
56 table = Table('user_ssh_keys', Base.metadata, autoload=False)
58 atime = datetime.datetime.utcnow()
57 atime = datetime.datetime.utcnow()
59 stmt = (
58 stmt = (
60 table.update()
59 table.update()
61 .where(table.c.ssh_key_id == key_id)
60 .where(table.c.ssh_key_id == key_id)
62 .values(accessed_on=atime)
61 .values(accessed_on=atime)
63 # no MySQL Support for .returning :((
62 # no MySQL Support for .returning :((
64 #.returning(table.c.accessed_on, table.c.ssh_key_fingerprint)
63 #.returning(table.c.accessed_on, table.c.ssh_key_fingerprint)
65 )
64 )
66
65
67 res_count = None
66 res_count = None
68 with raw_query_executor() as session:
67 with raw_query_executor() as session:
69 result = session.execute(stmt)
68 result = session.execute(stmt)
70 if result.rowcount:
69 if result.rowcount:
71 res_count = result.rowcount
70 res_count = result.rowcount
72
71
73 if res_count:
72 if res_count:
74 log.debug('Update key id:`%s` access time', key_id)
73 log.debug('Update key id:`%s` access time', key_id)
75
74
76 def get_user(self, user_id):
75 def get_user(self, user_id):
77 user = AttributeDict()
76 user = AttributeDict()
78 # lazy load db imports
77 # lazy load db imports
79 from rhodecode.model.db import User
78 from rhodecode.model.db import User
80 dbuser = User.get(user_id)
79 dbuser = User.get(user_id)
81 if not dbuser:
80 if not dbuser:
82 return None
81 return None
83 user.user_id = dbuser.user_id
82 user.user_id = dbuser.user_id
84 user.username = dbuser.username
83 user.username = dbuser.username
85 user.auth_user = dbuser.AuthUser()
84 user.auth_user = dbuser.AuthUser()
86 return user
85 return user
87
86
88 def get_connection_info(self):
87 def get_connection_info(self):
89 """
88 """
90 connection_info
89 connection_info
91
90
92 Identifies the client and server ends of the connection.
91 Identifies the client and server ends of the connection.
93 The variable contains four space-separated values: client IP address,
92 The variable contains four space-separated values: client IP address,
94 client port number, server IP address, and server port number.
93 client port number, server IP address, and server port number.
95 """
94 """
96 conn = dict(
95 conn = dict(
97 client_ip=None,
96 client_ip=None,
98 client_port=None,
97 client_port=None,
99 server_ip=None,
98 server_ip=None,
100 server_port=None,
99 server_port=None,
101 )
100 )
102
101
103 info = self.connection_info.split(' ')
102 info = self.connection_info.split(' ')
104 if len(info) == 4:
103 if len(info) == 4:
105 conn['client_ip'] = info[0]
104 conn['client_ip'] = info[0]
106 conn['client_port'] = info[1]
105 conn['client_port'] = info[1]
107 conn['server_ip'] = info[2]
106 conn['server_ip'] = info[2]
108 conn['server_port'] = info[3]
107 conn['server_port'] = info[3]
109
108
110 return conn
109 return conn
111
110
112 def maybe_translate_repo_uid(self, repo_name):
111 def maybe_translate_repo_uid(self, repo_name):
113 _org_name = repo_name
112 _org_name = repo_name
114 if _org_name.startswith('_'):
113 if _org_name.startswith('_'):
115 # remove format of _ID/subrepo
114 # remove format of _ID/subrepo
116 _org_name = _org_name.split('/', 1)[0]
115 _org_name = _org_name.split('/', 1)[0]
117
116
118 if repo_name.startswith('_'):
117 if repo_name.startswith('_'):
119 from rhodecode.model.repo import RepoModel
118 from rhodecode.model.repo import RepoModel
120 org_repo_name = repo_name
119 org_repo_name = repo_name
121 log.debug('translating UID repo %s', org_repo_name)
120 log.debug('translating UID repo %s', org_repo_name)
122 by_id_match = RepoModel().get_repo_by_id(repo_name)
121 by_id_match = RepoModel().get_repo_by_id(repo_name)
123 if by_id_match:
122 if by_id_match:
124 repo_name = by_id_match.repo_name
123 repo_name = by_id_match.repo_name
125 log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name)
124 log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name)
126
125
127 return repo_name, _org_name
126 return repo_name, _org_name
128
127
129 def get_repo_details(self, mode):
128 def get_repo_details(self, mode):
130 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
129 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
131 repo_name = None
130 repo_name = None
132
131
133 hg_match = self.hg_cmd_pat.match(self.command)
132 hg_match = self.hg_cmd_pat.match(self.command)
134 if hg_match is not None:
133 if hg_match is not None:
135 vcs_type = 'hg'
134 vcs_type = 'hg'
136 repo_id = hg_match.group(1).strip('/')
135 repo_id = hg_match.group(1).strip('/')
137 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
136 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
138 return vcs_type, repo_name, mode
137 return vcs_type, repo_name, mode
139
138
140 git_match = self.git_cmd_pat.match(self.command)
139 git_match = self.git_cmd_pat.match(self.command)
141 if git_match is not None:
140 if git_match is not None:
142 mode = git_match.group(1)
141 mode = git_match.group(1)
143 vcs_type = 'git'
142 vcs_type = 'git'
144 repo_id = git_match.group(2).strip('/')
143 repo_id = git_match.group(2).strip('/')
145 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
144 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
146 return vcs_type, repo_name, mode
145 return vcs_type, repo_name, mode
147
146
148 svn_match = self.svn_cmd_pat.match(self.command)
147 svn_match = self.svn_cmd_pat.match(self.command)
149 if svn_match is not None:
148 if svn_match is not None:
150 vcs_type = 'svn'
149 vcs_type = 'svn'
151 # Repo name should be extracted from the input stream, we're unable to
150 # Repo name should be extracted from the input stream, we're unable to
152 # extract it at this point in execution
151 # extract it at this point in execution
153 return vcs_type, repo_name, mode
152 return vcs_type, repo_name, mode
154
153
155 return vcs_type, repo_name, mode
154 return vcs_type, repo_name, mode
156
155
157 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
156 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
158 # TODO: remove this once we have .ini defined access path...
157 # TODO: remove this once we have .ini defined access path...
159 from rhodecode.model.scm import ScmModel
158 from rhodecode.model.scm import ScmModel
160
159
161 store = ScmModel().repos_path
160 store = ScmModel().repos_path
162
161
163 check_branch_perms = False
162 check_branch_perms = False
164 detect_force_push = False
163 detect_force_push = False
165
164
166 if branch_permissions:
165 if branch_permissions:
167 check_branch_perms = True
166 check_branch_perms = True
168 detect_force_push = True
167 detect_force_push = True
169
168
170 log.debug(
169 log.debug(
171 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
170 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
172 vcs, mode, repo, check_branch_perms)
171 vcs, mode, repo, check_branch_perms)
173
172
174 # detect if we have to check branch permissions
173 # detect if we have to check branch permissions
175 extras = {
174 extras = {
176 'detect_force_push': detect_force_push,
175 'detect_force_push': detect_force_push,
177 'check_branch_perms': check_branch_perms,
176 'check_branch_perms': check_branch_perms,
178 'config': self.ini_path
177 'config': self.ini_path
179 }
178 }
180
179
181 if vcs == 'hg':
180 if vcs == 'hg':
182 server = MercurialServer(
181 server = MercurialServer(
183 store=store, ini_path=self.ini_path,
182 store=store, ini_path=self.ini_path,
184 repo_name=repo, user=user,
183 repo_name=repo, user=user,
185 user_permissions=permissions, settings=self.settings, env=self.env)
184 user_permissions=permissions, settings=self.settings, env=self.env)
186 self.server_impl = server
185 self.server_impl = server
187 return server.run(tunnel_extras=extras)
186 return server.run(tunnel_extras=extras)
188
187
189 elif vcs == 'git':
188 elif vcs == 'git':
190 server = GitServer(
189 server = GitServer(
191 store=store, ini_path=self.ini_path,
190 store=store, ini_path=self.ini_path,
192 repo_name=repo, repo_mode=mode, user=user,
191 repo_name=repo, repo_mode=mode, user=user,
193 user_permissions=permissions, settings=self.settings, env=self.env)
192 user_permissions=permissions, settings=self.settings, env=self.env)
194 self.server_impl = server
193 self.server_impl = server
195 return server.run(tunnel_extras=extras)
194 return server.run(tunnel_extras=extras)
196
195
197 elif vcs == 'svn':
196 elif vcs == 'svn':
198 server = SubversionServer(
197 server = SubversionServer(
199 store=store, ini_path=self.ini_path,
198 store=store, ini_path=self.ini_path,
200 repo_name=None, user=user,
199 repo_name=None, user=user,
201 user_permissions=permissions, settings=self.settings, env=self.env)
200 user_permissions=permissions, settings=self.settings, env=self.env)
202 self.server_impl = server
201 self.server_impl = server
203 return server.run(tunnel_extras=extras)
202 return server.run(tunnel_extras=extras)
204
203
205 else:
204 else:
206 raise Exception(f'Unrecognised VCS: {vcs}')
205 raise Exception(f'Unrecognised VCS: {vcs}')
207
206
208 def wrap(self):
207 def wrap(self):
209 mode = self.mode
208 mode = self.mode
210 username = self.username
209 username = self.username
211 user_id = self.user_id
210 user_id = self.user_id
212 key_id = self.key_id
211 key_id = self.key_id
213 shell = self.shell
212 shell = self.shell
214
213
215 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
214 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
216
215
217 log.debug(
216 log.debug(
218 'Mode: `%s` User: `name:%s : id:%s` Shell: `%s` SSH Command: `\"%s\"` '
217 'Mode: `%s` User: `name:%s : id:%s` Shell: `%s` SSH Command: `\"%s\"` '
219 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
218 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
220 mode, username, user_id, shell, self.command,
219 mode, username, user_id, shell, self.command,
221 scm_detected, scm_mode, scm_repo)
220 scm_detected, scm_mode, scm_repo)
222
221
223 log.debug('SSH Connection info %s', self.get_connection_info())
222 log.debug('SSH Connection info %s', self.get_connection_info())
224
223
225 # update last access time for this key
224 # update last access time for this key
226 if key_id:
225 if key_id:
227 self.update_key_access_time(key_id)
226 self.update_key_access_time(key_id)
228
227
229 if shell and self.command is None:
228 if shell and self.command is None:
230 log.info('Dropping to shell, no command given and shell is allowed')
229 log.info('Dropping to shell, no command given and shell is allowed')
231 os.execl('/bin/bash', '-l')
230 os.execl('/bin/bash', '-l')
232 exit_code = 1
231 exit_code = 1
233
232
234 elif scm_detected:
233 elif scm_detected:
235 user = self.get_user(user_id)
234 user = self.get_user(user_id)
236 if not user:
235 if not user:
237 log.warning('User with id %s not found', user_id)
236 log.warning('User with id %s not found', user_id)
238 exit_code = -1
237 exit_code = -1
239 return exit_code
238 return exit_code
240
239
241 auth_user = user.auth_user
240 auth_user = user.auth_user
242 permissions = auth_user.permissions['repositories']
241 permissions = auth_user.permissions['repositories']
243 repo_branch_permissions = auth_user.get_branch_permissions(scm_repo)
242 repo_branch_permissions = auth_user.get_branch_permissions(scm_repo)
244 try:
243 try:
245 exit_code, is_updated = self.serve(
244 exit_code, is_updated = self.serve(
246 scm_detected, scm_repo, scm_mode, user, permissions,
245 scm_detected, scm_repo, scm_mode, user, permissions,
247 repo_branch_permissions)
246 repo_branch_permissions)
248 except Exception:
247 except Exception:
249 log.exception('Error occurred during execution of SshWrapper')
248 log.exception('Error occurred during execution of SshWrapper')
250 exit_code = -1
249 exit_code = -1
251
250
252 elif self.command is None and shell is False:
251 elif self.command is None and shell is False:
253 log.error('No Command given.')
252 log.error('No Command given.')
254 exit_code = -1
253 exit_code = -1
255
254
256 else:
255 else:
257 log.error('Unhandled Command: "%s" Aborting.', self.command)
256 log.error('Unhandled Command: "%s" Aborting.', self.command)
258 exit_code = -1
257 exit_code = -1
259
258
260 return exit_code
259 return exit_code
261
260
262
261
263 class SshWrapperStandalone(SshWrapper):
262 class SshWrapperStandalone(SshWrapper):
264 """
263 """
265 New version of SshWrapper designed to be depended only on service API
264 New version of SshWrapper designed to be depended only on service API
266 """
265 """
267 repos_path = None
266 repos_path = None
268 service_api_host: str
269 service_api_token: str
270 api_url: str
271
272 def __init__(self, command, connection_info, mode,
273 user, user_id, key_id: int, shell, ini_path: str, settings, env):
274
275 # validate our settings for making a standalone calls
276 try:
277 self.service_api_host = settings['app.service_api.host']
278 self.service_api_token = settings['app.service_api.token']
279 except KeyError:
280 raise ImproperlyConfiguredError(
281 "app.service_api.host or app.service_api.token are missing. "
282 "Please ensure that app.service_api.host and app.service_api.token are "
283 "defined inside of .ini configuration file."
284 )
285
286 try:
287 self.api_url = settings['rhodecode.api.url']
288 except KeyError:
289 raise ImproperlyConfiguredError(
290 "rhodecode.api.url is missing. "
291 "Please ensure that rhodecode.api.url is "
292 "defined inside of .ini configuration file."
293 )
294
295 super(SshWrapperStandalone, self).__init__(
296 command, connection_info, mode, user, user_id, key_id, shell, ini_path, settings, env)
297
267
298 @staticmethod
268 @staticmethod
299 def parse_user_related_data(user_data):
269 def parse_user_related_data(user_data):
300 user = AttributeDict()
270 user = AttributeDict()
301 user.user_id = user_data['user_id']
271 user.user_id = user_data['user_id']
302 user.username = user_data['username']
272 user.username = user_data['username']
303 user.repo_permissions = user_data['repo_permissions']
273 user.repo_permissions = user_data['repo_permissions']
304 user.branch_permissions = user_data['branch_permissions']
274 user.branch_permissions = user_data['branch_permissions']
305 return user
275 return user
306
276
307 def wrap(self):
277 def wrap(self):
308 mode = self.mode
278 mode = self.mode
309 username = self.username
279 username = self.username
310 user_id = self.user_id
280 user_id = self.user_id
311 shell = self.shell
281 shell = self.shell
312
282
313 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
283 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
314
284
315 log.debug(
285 log.debug(
316 'Mode: `%s` User: `name:%s : id:%s` Shell: `%s` SSH Command: `\"%s\"` '
286 'Mode: `%s` User: `name:%s : id:%s` Shell: `%s` SSH Command: `\"%s\"` '
317 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
287 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
318 mode, username, user_id, shell, self.command,
288 mode, username, user_id, shell, self.command,
319 scm_detected, scm_mode, scm_repo)
289 scm_detected, scm_mode, scm_repo)
320
290
321 log.debug('SSH Connection info %s', self.get_connection_info())
291 log.debug('SSH Connection info %s', self.get_connection_info())
322
292
323 if shell and self.command is None:
293 if shell and self.command is None:
324 log.info('Dropping to shell, no command given and shell is allowed')
294 log.info('Dropping to shell, no command given and shell is allowed')
325 os.execl('/bin/bash', '-l')
295 os.execl('/bin/bash', '-l')
326 exit_code = 1
296 exit_code = 1
327
297
328 elif scm_detected:
298 elif scm_detected:
329 data = call_service_api(self.service_api_host, self.service_api_token, self.api_url, {
299 data = call_service_api(self.settings, {
330 "method": "service_get_data_for_ssh_wrapper",
300 "method": "service_get_data_for_ssh_wrapper",
331 "args": {"user_id": user_id, "repo_name": scm_repo, "key_id": self.key_id}
301 "args": {"user_id": user_id, "repo_name": scm_repo, "key_id": self.key_id}
332 })
302 })
333 user = self.parse_user_related_data(data)
303 user = self.parse_user_related_data(data)
334 if not user:
304 if not user:
335 log.warning('User with id %s not found', user_id)
305 log.warning('User with id %s not found', user_id)
336 exit_code = -1
306 exit_code = -1
337 return exit_code
307 return exit_code
338 self.repos_path = data['repos_path']
308 self.repos_path = data['repos_path']
339 permissions = user.repo_permissions
309 permissions = user.repo_permissions
340 repo_branch_permissions = user.branch_permissions
310 repo_branch_permissions = user.branch_permissions
341 try:
311 try:
342 exit_code, is_updated = self.serve(
312 exit_code, is_updated = self.serve(
343 scm_detected, scm_repo, scm_mode, user, permissions,
313 scm_detected, scm_repo, scm_mode, user, permissions,
344 repo_branch_permissions)
314 repo_branch_permissions)
345 except Exception:
315 except Exception:
346 log.exception('Error occurred during execution of SshWrapper')
316 log.exception('Error occurred during execution of SshWrapper')
347 exit_code = -1
317 exit_code = -1
348
318
349 elif self.command is None and shell is False:
319 elif self.command is None and shell is False:
350 log.error('No Command given.')
320 log.error('No Command given.')
351 exit_code = -1
321 exit_code = -1
352
322
353 else:
323 else:
354 log.error('Unhandled Command: "%s" Aborting.', self.command)
324 log.error('Unhandled Command: "%s" Aborting.', self.command)
355 exit_code = -1
325 exit_code = -1
356
326
357 return exit_code
327 return exit_code
358
328
359 def maybe_translate_repo_uid(self, repo_name):
329 def maybe_translate_repo_uid(self, repo_name):
360 _org_name = repo_name
330 _org_name = repo_name
361 if _org_name.startswith('_'):
331 if _org_name.startswith('_'):
362 _org_name = _org_name.split('/', 1)[0]
332 _org_name = _org_name.split('/', 1)[0]
363
333
364 if repo_name.startswith('_'):
334 if repo_name.startswith('_'):
365 org_repo_name = repo_name
335 org_repo_name = repo_name
366 log.debug('translating UID repo %s', org_repo_name)
336 log.debug('translating UID repo %s', org_repo_name)
367 by_id_match = call_service_api(self.service_api_host, self.service_api_token, self.api_url, {
337 by_id_match = call_service_api(self.settings, {
368 'method': 'service_get_repo_name_by_id',
338 'method': 'service_get_repo_name_by_id',
369 "args": {"repo_id": repo_name}
339 "args": {"repo_id": repo_name}
370 })
340 })
371 if by_id_match:
341 if by_id_match:
372 repo_name = by_id_match['repo_name']
342 repo_name = by_id_match['repo_name']
373 log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name)
343 log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name)
374
344
375 return repo_name, _org_name
345 return repo_name, _org_name
376
346
377 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
347 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
378 store = self.repos_path
348 store = self.repos_path
379
349
380 check_branch_perms = False
350 check_branch_perms = False
381 detect_force_push = False
351 detect_force_push = False
382
352
383 if branch_permissions:
353 if branch_permissions:
384 check_branch_perms = True
354 check_branch_perms = True
385 detect_force_push = True
355 detect_force_push = True
386
356
387 log.debug(
357 log.debug(
388 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
358 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
389 vcs, mode, repo, check_branch_perms)
359 vcs, mode, repo, check_branch_perms)
390
360
391 # detect if we have to check branch permissions
361 # detect if we have to check branch permissions
392 extras = {
362 extras = {
393 'detect_force_push': detect_force_push,
363 'detect_force_push': detect_force_push,
394 'check_branch_perms': check_branch_perms,
364 'check_branch_perms': check_branch_perms,
395 'config': self.ini_path
365 'config': self.ini_path
396 }
366 }
397
367
398 match vcs:
368 match vcs:
399 case 'hg':
369 case 'hg':
400 server = MercurialServer(
370 server = MercurialServer(
401 store=store, ini_path=self.ini_path,
371 store=store, ini_path=self.ini_path,
402 repo_name=repo, user=user,
372 repo_name=repo, user=user,
403 user_permissions=permissions, settings=self.settings, env=self.env)
373 user_permissions=permissions, settings=self.settings, env=self.env)
404 case 'git':
374 case 'git':
405 server = GitServer(
375 server = GitServer(
406 store=store, ini_path=self.ini_path,
376 store=store, ini_path=self.ini_path,
407 repo_name=repo, repo_mode=mode, user=user,
377 repo_name=repo, repo_mode=mode, user=user,
408 user_permissions=permissions, settings=self.settings, env=self.env)
378 user_permissions=permissions, settings=self.settings, env=self.env)
409 case 'svn':
379 case 'svn':
410 server = SubversionServer(
380 server = SubversionServer(
411 store=store, ini_path=self.ini_path,
381 store=store, ini_path=self.ini_path,
412 repo_name=None, user=user,
382 repo_name=None, user=user,
413 user_permissions=permissions, settings=self.settings, env=self.env)
383 user_permissions=permissions, settings=self.settings, env=self.env)
414 case _:
384 case _:
415 raise Exception(f'Unrecognised VCS: {vcs}')
385 raise Exception(f'Unrecognised VCS: {vcs}')
416 self.server_impl = server
386 self.server_impl = server
417 return server.run(tunnel_extras=extras)
387 return server.run(tunnel_extras=extras)
@@ -1,179 +1,175 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 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
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
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/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
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
19 import os
19 import os
20 import sys
20 import sys
21 import logging
21 import logging
22
22
23 from rhodecode.lib.hook_daemon.base 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.lib.api_utils import call_service_api
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 SSHVcsServer(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 settings = None # parsed settings module
37 write_perms = ['repository.admin', 'repository.write']
37 write_perms = ['repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39
39
40 def __init__(self, user, user_permissions, settings, env):
40 def __init__(self, user, user_permissions, settings, env):
41 self.user = user
41 self.user = user
42 self.user_permissions = user_permissions
42 self.user_permissions = user_permissions
43 self.settings = settings
43 self.settings = settings
44 self.env = env
44 self.env = env
45 self.stdin = sys.stdin
45 self.stdin = sys.stdin
46
46
47 self.repo_name = None
47 self.repo_name = None
48 self.repo_mode = None
48 self.repo_mode = None
49 self.store = ''
49 self.store = ''
50 self.ini_path = ''
50 self.ini_path = ''
51 self.hooks_protocol = None
51 self.hooks_protocol = None
52
52
53 def _invalidate_cache(self, repo_name):
53 def _invalidate_cache(self, repo_name):
54 """
54 """
55 Set's cache for this repository for invalidation on next access
55 Set's cache for this repository for invalidation on next access
56
56
57 :param repo_name: full repo name, also a cache key
57 :param repo_name: full repo name, also a cache key
58 """
58 """
59 # Todo: Leave only "celery" case after transition.
59 # Todo: Leave only "celery" case after transition.
60 match self.hooks_protocol:
60 match self.hooks_protocol:
61 case 'http':
61 case 'http':
62 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.scm import ScmModel
63 ScmModel().mark_for_invalidation(repo_name)
63 ScmModel().mark_for_invalidation(repo_name)
64 case 'celery':
64 case 'celery':
65 service_api_host = self.settings['app.service_api.host']
65 call_service_api(self.settings, {
66 service_api_token = self.settings['app.service_api.token']
67 api_url = self.settings['rhodecode.api.url']
68
69 call_service_api(service_api_host, service_api_token, api_url, {
70 "method": "service_mark_for_invalidation",
66 "method": "service_mark_for_invalidation",
71 "args": {"repo_name": repo_name}
67 "args": {"repo_name": repo_name}
72 })
68 })
73
69
74 def has_write_perm(self):
70 def has_write_perm(self):
75 permission = self.user_permissions.get(self.repo_name)
71 permission = self.user_permissions.get(self.repo_name)
76 if permission in ['repository.write', 'repository.admin']:
72 if permission in ['repository.write', 'repository.admin']:
77 return True
73 return True
78
74
79 return False
75 return False
80
76
81 def _check_permissions(self, action):
77 def _check_permissions(self, action):
82 permission = self.user_permissions.get(self.repo_name)
78 permission = self.user_permissions.get(self.repo_name)
83 user_info = f'{self.user["user_id"]}:{self.user["username"]}'
79 user_info = f'{self.user["user_id"]}:{self.user["username"]}'
84 log.debug('permission for %s on %s are: %s',
80 log.debug('permission for %s on %s are: %s',
85 user_info, self.repo_name, permission)
81 user_info, self.repo_name, permission)
86
82
87 if not permission:
83 if not permission:
88 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.',
89 user_info, self.repo_name)
85 user_info, self.repo_name)
90 return -2
86 return -2
91
87
92 if action == 'pull':
88 if action == 'pull':
93 if permission in self.read_perms:
89 if permission in self.read_perms:
94 log.info(
90 log.info(
95 'READ Permissions for User "%s" detected to repo "%s"!',
91 'READ Permissions for User "%s" detected to repo "%s"!',
96 user_info, self.repo_name)
92 user_info, self.repo_name)
97 return 0
93 return 0
98 else:
94 else:
99 if permission in self.write_perms:
95 if permission in self.write_perms:
100 log.info(
96 log.info(
101 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
97 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
102 user_info, self.repo_name)
98 user_info, self.repo_name)
103 return 0
99 return 0
104
100
105 log.error('Cannot properly fetch or verify user `%s` permissions. '
101 log.error('Cannot properly fetch or verify user `%s` permissions. '
106 'Permissions: %s, vcs action: %s',
102 'Permissions: %s, vcs action: %s',
107 user_info, permission, action)
103 user_info, permission, action)
108 return -2
104 return -2
109
105
110 def update_environment(self, action, extras=None):
106 def update_environment(self, action, extras=None):
111
107
112 scm_data = {
108 scm_data = {
113 'ip': os.environ['SSH_CLIENT'].split()[0],
109 'ip': os.environ['SSH_CLIENT'].split()[0],
114 'username': self.user.username,
110 'username': self.user.username,
115 'user_id': self.user.user_id,
111 'user_id': self.user.user_id,
116 'action': action,
112 'action': action,
117 'repository': self.repo_name,
113 'repository': self.repo_name,
118 'scm': self.backend,
114 'scm': self.backend,
119 'config': self.ini_path,
115 'config': self.ini_path,
120 'repo_store': self.store,
116 'repo_store': self.store,
121 'make_lock': None,
117 'make_lock': None,
122 'locked_by': [None, None],
118 'locked_by': [None, None],
123 'server_url': None,
119 'server_url': None,
124 'user_agent': f'{self.repo_user_agent}/ssh-user-agent',
120 'user_agent': f'{self.repo_user_agent}/ssh-user-agent',
125 'hooks': ['push', 'pull'],
121 'hooks': ['push', 'pull'],
126 'hooks_module': 'rhodecode.lib.hook_daemon.hook_module',
122 'hooks_module': 'rhodecode.lib.hook_daemon.hook_module',
127 'is_shadow_repo': False,
123 'is_shadow_repo': False,
128 'detect_force_push': False,
124 'detect_force_push': False,
129 'check_branch_perms': False,
125 'check_branch_perms': False,
130
126
131 'SSH': True,
127 'SSH': True,
132 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
128 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
133 }
129 }
134 if extras:
130 if extras:
135 scm_data.update(extras)
131 scm_data.update(extras)
136 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
132 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
137 return scm_data
133 return scm_data
138
134
139 def get_root_store(self):
135 def get_root_store(self):
140 root_store = self.store
136 root_store = self.store
141 if not root_store.endswith('/'):
137 if not root_store.endswith('/'):
142 # always append trailing slash
138 # always append trailing slash
143 root_store = root_store + '/'
139 root_store = root_store + '/'
144 return root_store
140 return root_store
145
141
146 def _handle_tunnel(self, extras):
142 def _handle_tunnel(self, extras):
147 # pre-auth
143 # pre-auth
148 action = 'pull'
144 action = 'pull'
149 exit_code = self._check_permissions(action)
145 exit_code = self._check_permissions(action)
150 if exit_code:
146 if exit_code:
151 return exit_code, False
147 return exit_code, False
152
148
153 req = self.env.get('request')
149 req = self.env.get('request')
154 if req:
150 if req:
155 server_url = req.host_url + req.script_name
151 server_url = req.host_url + req.script_name
156 extras['server_url'] = server_url
152 extras['server_url'] = server_url
157
153
158 log.debug('Using %s binaries from path %s', self.backend, self._path)
154 log.debug('Using %s binaries from path %s', self.backend, self._path)
159 exit_code = self.tunnel.run(extras)
155 exit_code = self.tunnel.run(extras)
160
156
161 return exit_code, action == "push"
157 return exit_code, action == "push"
162
158
163 def run(self, tunnel_extras=None):
159 def run(self, tunnel_extras=None):
164 self.hooks_protocol = self.settings['vcs.hooks.protocol']
160 self.hooks_protocol = self.settings['vcs.hooks.protocol']
165 tunnel_extras = tunnel_extras or {}
161 tunnel_extras = tunnel_extras or {}
166 extras = {}
162 extras = {}
167 extras.update(tunnel_extras)
163 extras.update(tunnel_extras)
168
164
169 callback_daemon, extras = prepare_callback_daemon(
165 callback_daemon, extras = prepare_callback_daemon(
170 extras, protocol=self.hooks_protocol,
166 extras, protocol=self.hooks_protocol,
171 host=vcs_settings.HOOKS_HOST)
167 host=vcs_settings.HOOKS_HOST)
172
168
173 with callback_daemon:
169 with callback_daemon:
174 try:
170 try:
175 return self._handle_tunnel(extras)
171 return self._handle_tunnel(extras)
176 finally:
172 finally:
177 log.debug('Running cleanup with cache invalidation')
173 log.debug('Running cleanup with cache invalidation')
178 if self.repo_name:
174 if self.repo_name:
179 self._invalidate_cache(self.repo_name)
175 self._invalidate_cache(self.repo_name)
@@ -1,86 +1,86 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 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
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
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/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
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
19 import sys
19 import sys
20 import logging
20 import logging
21 import subprocess
21 import subprocess
22
22
23 from vcsserver import hooks
23 from vcsserver import hooks
24 from .base import SSHVcsServer
24 from .base import SshVcsServer
25
25
26 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
27
27
28
28
29 class GitTunnelWrapper(object):
29 class GitTunnelWrapper(object):
30 process = None
30 process = None
31
31
32 def __init__(self, server):
32 def __init__(self, server):
33 self.server = server
33 self.server = server
34 self.stdin = sys.stdin
34 self.stdin = sys.stdin
35 self.stdout = sys.stdout
35 self.stdout = sys.stdout
36
36
37 def create_hooks_env(self):
37 def create_hooks_env(self):
38 pass
38 pass
39
39
40 def command(self):
40 def command(self):
41 root = self.server.get_root_store()
41 root = self.server.get_root_store()
42 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
42 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
43 root=root, git_path=self.server.git_path,
43 root=root, git_path=self.server.git_path,
44 mode=self.server.repo_mode, repo_name=self.server.repo_name)
44 mode=self.server.repo_mode, repo_name=self.server.repo_name)
45 log.debug("Final CMD: %s", command)
45 log.debug("Final CMD: %s", command)
46 return command
46 return command
47
47
48 def run(self, extras):
48 def run(self, extras):
49 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
49 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
50 exit_code = self.server._check_permissions(action)
50 exit_code = self.server._check_permissions(action)
51 if exit_code:
51 if exit_code:
52 return exit_code
52 return exit_code
53
53
54 scm_extras = self.server.update_environment(action=action, extras=extras)
54 scm_extras = self.server.update_environment(action=action, extras=extras)
55
55
56 if action == "pull":
56 if action == "pull":
57 hook_response = hooks.git_pre_pull(scm_extras)
57 hook_response = hooks.git_pre_pull(scm_extras)
58 pre_pull_messages = hook_response.output
58 pre_pull_messages = hook_response.output
59 sys.stdout.write(pre_pull_messages)
59 sys.stdout.write(pre_pull_messages)
60
60
61 self.create_hooks_env()
61 self.create_hooks_env()
62 result = subprocess.run(self.command(), shell=True)
62 result = subprocess.run(self.command(), shell=True)
63 result = result.returncode
63 result = result.returncode
64
64
65 # Upload-pack == clone
65 # Upload-pack == clone
66 if action == "pull":
66 if action == "pull":
67 hook_response = hooks.git_post_pull(scm_extras)
67 hook_response = hooks.git_post_pull(scm_extras)
68 post_pull_messages = hook_response.output
68 post_pull_messages = hook_response.output
69 sys.stderr.write(post_pull_messages)
69 sys.stderr.write(post_pull_messages)
70 return result
70 return result
71
71
72
72
73 class GitServer(SSHVcsServer):
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, user, user_permissions, settings, env):
77 def __init__(self, store, ini_path, repo_name, repo_mode, user, user_permissions, settings, env):
78 super().__init__(user, user_permissions, settings, env)
78 super().__init__(user, user_permissions, settings, env)
79
79
80 self.store = store
80 self.store = store
81 self.ini_path = ini_path
81 self.ini_path = ini_path
82 self.repo_name = repo_name
82 self.repo_name = repo_name
83 self._path = self.git_path = settings['ssh.executable.git']
83 self._path = self.git_path = settings['ssh.executable.git']
84
84
85 self.repo_mode = repo_mode
85 self.repo_mode = repo_mode
86 self.tunnel = GitTunnelWrapper(server=self)
86 self.tunnel = GitTunnelWrapper(server=self)
@@ -1,160 +1,157 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 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
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
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/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
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
19 import os
19 import os
20 import sys
20 import sys
21 import logging
21 import logging
22 import tempfile
22 import tempfile
23 import textwrap
23 import textwrap
24 import collections
24 import collections
25
25
26 from .base import SSHVcsServer
26 from .base import SshVcsServer
27
27
28 from rhodecode.lib.api_utils import call_service_api
28 from rhodecode.lib.api_utils import call_service_api
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class MercurialTunnelWrapper(object):
33 class MercurialTunnelWrapper(object):
34 process = None
34 process = None
35
35
36 def __init__(self, server):
36 def __init__(self, server):
37 self.server = server
37 self.server = server
38 self.stdin = sys.stdin
38 self.stdin = sys.stdin
39 self.stdout = sys.stdout
39 self.stdout = sys.stdout
40 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp(prefix='hgrc_rhodecode_')
40 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp(prefix='hgrc_rhodecode_')
41
41
42 def create_hooks_env(self):
42 def create_hooks_env(self):
43 repo_name = self.server.repo_name
43 repo_name = self.server.repo_name
44 hg_flags = self.server.config_to_hgrc(repo_name)
44 hg_flags = self.server.config_to_hgrc(repo_name)
45
45
46 content = textwrap.dedent(
46 content = textwrap.dedent(
47 '''
47 '''
48 # RhodeCode SSH hooks version=2.0.0
48 # RhodeCode SSH hooks version=2.0.0
49 {custom}
49 {custom}
50 '''
50 '''
51 ).format(custom='\n'.join(hg_flags))
51 ).format(custom='\n'.join(hg_flags))
52
52
53 root = self.server.get_root_store()
53 root = self.server.get_root_store()
54 hgrc_custom = os.path.join(root, repo_name, '.hg', 'hgrc_rhodecode')
54 hgrc_custom = os.path.join(root, repo_name, '.hg', 'hgrc_rhodecode')
55 hgrc_main = os.path.join(root, repo_name, '.hg', 'hgrc')
55 hgrc_main = os.path.join(root, repo_name, '.hg', 'hgrc')
56
56
57 # cleanup custom hgrc file
57 # cleanup custom hgrc file
58 if os.path.isfile(hgrc_custom):
58 if os.path.isfile(hgrc_custom):
59 with open(hgrc_custom, 'wb') as f:
59 with open(hgrc_custom, 'wb') as f:
60 f.write(b'')
60 f.write(b'')
61 log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
61 log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
62
62
63 # write temp
63 # write temp
64 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
64 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
65 hooks_env_file.write(content)
65 hooks_env_file.write(content)
66
66
67 return self.hooks_env_path
67 return self.hooks_env_path
68
68
69 def remove_configs(self):
69 def remove_configs(self):
70 os.remove(self.hooks_env_path)
70 os.remove(self.hooks_env_path)
71
71
72 def command(self, hgrc_path):
72 def command(self, hgrc_path):
73 root = self.server.get_root_store()
73 root = self.server.get_root_store()
74
74
75 command = (
75 command = (
76 "cd {root}; HGRCPATH={hgrc} {hg_path} -R {root}{repo_name} "
76 "cd {root}; HGRCPATH={hgrc} {hg_path} -R {root}{repo_name} "
77 "serve --stdio".format(
77 "serve --stdio".format(
78 root=root, hg_path=self.server.hg_path,
78 root=root, hg_path=self.server.hg_path,
79 repo_name=self.server.repo_name, hgrc=hgrc_path))
79 repo_name=self.server.repo_name, hgrc=hgrc_path))
80 log.debug("Final CMD: %s", command)
80 log.debug("Final CMD: %s", command)
81 return command
81 return command
82
82
83 def run(self, extras):
83 def run(self, extras):
84 # at this point we cannot tell, we do further ACL checks
84 # at this point we cannot tell, we do further ACL checks
85 # inside the hooks
85 # inside the hooks
86 action = '?'
86 action = '?'
87 # permissions are check via `pre_push_ssh_auth` hook
87 # permissions are check via `pre_push_ssh_auth` hook
88 self.server.update_environment(action=action, extras=extras)
88 self.server.update_environment(action=action, extras=extras)
89 custom_hgrc_file = self.create_hooks_env()
89 custom_hgrc_file = self.create_hooks_env()
90
90
91 try:
91 try:
92 return os.system(self.command(custom_hgrc_file))
92 return os.system(self.command(custom_hgrc_file))
93 finally:
93 finally:
94 self.remove_configs()
94 self.remove_configs()
95
95
96
96
97 class MercurialServer(SSHVcsServer):
97 class MercurialServer(SshVcsServer):
98 backend = 'hg'
98 backend = 'hg'
99 repo_user_agent = 'mercurial'
99 repo_user_agent = 'mercurial'
100 cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
100 cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
101
101
102 def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env):
102 def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env):
103 super().__init__(user, user_permissions, settings, env)
103 super().__init__(user, user_permissions, settings, env)
104
104
105 self.store = store
105 self.store = store
106 self.ini_path = ini_path
106 self.ini_path = ini_path
107 self.repo_name = repo_name
107 self.repo_name = repo_name
108 self._path = self.hg_path = settings['ssh.executable.hg']
108 self._path = self.hg_path = settings['ssh.executable.hg']
109 self.tunnel = MercurialTunnelWrapper(server=self)
109 self.tunnel = MercurialTunnelWrapper(server=self)
110
110
111 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
112 # Todo: once transition is done only call to service api should exist
113 if self.hooks_protocol == 'celery':
113 if self.hooks_protocol == 'celery':
114 service_api_host = self.settings['app.service_api.host']
114 data = call_service_api(self.settings, {
115 service_api_token = self.settings['app.service_api.token']
116 api_url = self.settings['rhodecode.api.url']
117 data = call_service_api(service_api_host, service_api_token, api_url, {
118 "method": "service_config_to_hgrc",
115 "method": "service_config_to_hgrc",
119 "args": {"cli_flags": self.cli_flags, "repo_name": repo_name}
116 "args": {"cli_flags": self.cli_flags, "repo_name": repo_name}
120 })
117 })
121 return data['flags']
118 return data['flags']
122 else:
119 else:
123 from rhodecode.model.db import RhodeCodeUi
120 from rhodecode.model.db import RhodeCodeUi
124 from rhodecode.model.settings import VcsSettingsModel
121 from rhodecode.model.settings import VcsSettingsModel
125 ui_sections = collections.defaultdict(list)
122 ui_sections = collections.defaultdict(list)
126 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)
127
124
128 # write default hooks
125 # write default hooks
129 default_hooks = [
126 default_hooks = [
130 ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'),
127 ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'),
131 ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'),
128 ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'),
132 ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'),
129 ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'),
133
130
134 ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'),
131 ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'),
135 ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'),
132 ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'),
136 ]
133 ]
137
134
138 for k, v in default_hooks:
135 for k, v in default_hooks:
139 ui_sections['hooks'].append((k, v))
136 ui_sections['hooks'].append((k, v))
140
137
141 for entry in ui:
138 for entry in ui:
142 if not entry.active:
139 if not entry.active:
143 continue
140 continue
144 sec = entry.section
141 sec = entry.section
145 key = entry.key
142 key = entry.key
146
143
147 if sec in self.cli_flags:
144 if sec in self.cli_flags:
148 # we want only custom hooks, so we skip builtins
145 # we want only custom hooks, so we skip builtins
149 if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN:
146 if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN:
150 continue
147 continue
151
148
152 ui_sections[sec].append([key, entry.value])
149 ui_sections[sec].append([key, entry.value])
153
150
154 flags = []
151 flags = []
155 for _sec, key_val in ui_sections.items():
152 for _sec, key_val in ui_sections.items():
156 flags.append(' ')
153 flags.append(' ')
157 flags.append(f'[{_sec}]')
154 flags.append(f'[{_sec}]')
158 for key, val in key_val:
155 for key, val in key_val:
159 flags.append(f'{key}= {val}')
156 flags.append(f'{key}= {val}')
160 return flags
157 return flags
@@ -1,254 +1,254 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 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
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
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/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
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
19 import os
19 import os
20 import re
20 import re
21 import sys
21 import sys
22 import logging
22 import logging
23 import signal
23 import signal
24 import tempfile
24 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 SSHVcsServer
28 from .base import SshVcsServer
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class SubversionTunnelWrapper(object):
33 class SubversionTunnelWrapper(object):
34 process = None
34 process = None
35
35
36 def __init__(self, server):
36 def __init__(self, server):
37 self.server = server
37 self.server = server
38 self.timeout = 30
38 self.timeout = 30
39 self.stdin = sys.stdin
39 self.stdin = sys.stdin
40 self.stdout = sys.stdout
40 self.stdout = sys.stdout
41 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
41 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
42 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
42 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
43
43
44 self.read_only = True # flag that we set to make the hooks readonly
44 self.read_only = True # flag that we set to make the hooks readonly
45
45
46 def create_svn_config(self):
46 def create_svn_config(self):
47 content = (
47 content = (
48 '[general]\n'
48 '[general]\n'
49 'hooks-env = {}\n').format(self.hooks_env_path)
49 'hooks-env = {}\n').format(self.hooks_env_path)
50 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
50 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
51 config_file.write(content)
51 config_file.write(content)
52
52
53 def create_hooks_env(self):
53 def create_hooks_env(self):
54 content = (
54 content = (
55 '[default]\n'
55 '[default]\n'
56 'LANG = en_US.UTF-8\n')
56 'LANG = en_US.UTF-8\n')
57 if self.read_only:
57 if self.read_only:
58 content += 'SSH_READ_ONLY = 1\n'
58 content += 'SSH_READ_ONLY = 1\n'
59 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
59 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
60 hooks_env_file.write(content)
60 hooks_env_file.write(content)
61
61
62 def remove_configs(self):
62 def remove_configs(self):
63 os.remove(self.svn_conf_path)
63 os.remove(self.svn_conf_path)
64 os.remove(self.hooks_env_path)
64 os.remove(self.hooks_env_path)
65
65
66 def command(self):
66 def command(self):
67 root = self.server.get_root_store()
67 root = self.server.get_root_store()
68 username = self.server.user.username
68 username = self.server.user.username
69
69
70 command = [
70 command = [
71 self.server.svn_path, '-t',
71 self.server.svn_path, '-t',
72 '--config-file', self.svn_conf_path,
72 '--config-file', self.svn_conf_path,
73 '--tunnel-user', username,
73 '--tunnel-user', username,
74 '-r', root]
74 '-r', root]
75 log.debug("Final CMD: %s", ' '.join(command))
75 log.debug("Final CMD: %s", ' '.join(command))
76 return command
76 return command
77
77
78 def start(self):
78 def start(self):
79 command = self.command()
79 command = self.command()
80 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
80 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
81
81
82 def sync(self):
82 def sync(self):
83 while self.process.poll() is None:
83 while self.process.poll() is None:
84 next_byte = self.stdin.read(1)
84 next_byte = self.stdin.read(1)
85 if not next_byte:
85 if not next_byte:
86 break
86 break
87 self.process.stdin.write(next_byte)
87 self.process.stdin.write(next_byte)
88 self.remove_configs()
88 self.remove_configs()
89
89
90 @property
90 @property
91 def return_code(self):
91 def return_code(self):
92 return self.process.returncode
92 return self.process.returncode
93
93
94 def get_first_client_response(self):
94 def get_first_client_response(self):
95 signal.signal(signal.SIGALRM, self.interrupt)
95 signal.signal(signal.SIGALRM, self.interrupt)
96 signal.alarm(self.timeout)
96 signal.alarm(self.timeout)
97 first_response = self._read_first_client_response()
97 first_response = self._read_first_client_response()
98 signal.alarm(0)
98 signal.alarm(0)
99 return (self._parse_first_client_response(first_response)
99 return (self._parse_first_client_response(first_response)
100 if first_response else None)
100 if first_response else None)
101
101
102 def patch_first_client_response(self, response, **kwargs):
102 def patch_first_client_response(self, response, **kwargs):
103 self.create_hooks_env()
103 self.create_hooks_env()
104 data = response.copy()
104 data = response.copy()
105 data.update(kwargs)
105 data.update(kwargs)
106 data['url'] = self._svn_string(data['url'])
106 data['url'] = self._svn_string(data['url'])
107 data['ra_client'] = self._svn_string(data['ra_client'])
107 data['ra_client'] = self._svn_string(data['ra_client'])
108 data['client'] = data['client'] or ''
108 data['client'] = data['client'] or ''
109 buffer_ = (
109 buffer_ = (
110 "( {version} ( {capabilities} ) {url}{ra_client}"
110 "( {version} ( {capabilities} ) {url}{ra_client}"
111 "( {client}) ) ".format(**data))
111 "( {client}) ) ".format(**data))
112 self.process.stdin.write(buffer_)
112 self.process.stdin.write(buffer_)
113
113
114 def fail(self, message):
114 def fail(self, message):
115 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
115 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
116 message=self._svn_string(message)))
116 message=self._svn_string(message)))
117 self.remove_configs()
117 self.remove_configs()
118 self.process.kill()
118 self.process.kill()
119 return 1
119 return 1
120
120
121 def interrupt(self, signum, frame):
121 def interrupt(self, signum, frame):
122 self.fail("Exited by timeout")
122 self.fail("Exited by timeout")
123
123
124 def _svn_string(self, str_):
124 def _svn_string(self, str_):
125 if not str_:
125 if not str_:
126 return ''
126 return ''
127 return f'{len(str_)}:{str_} '
127 return f'{len(str_)}:{str_} '
128
128
129 def _read_first_client_response(self):
129 def _read_first_client_response(self):
130 buffer_ = ""
130 buffer_ = ""
131 brackets_stack = []
131 brackets_stack = []
132 while True:
132 while True:
133 next_byte = self.stdin.read(1)
133 next_byte = self.stdin.read(1)
134 buffer_ += next_byte
134 buffer_ += next_byte
135 if next_byte == "(":
135 if next_byte == "(":
136 brackets_stack.append(next_byte)
136 brackets_stack.append(next_byte)
137 elif next_byte == ")":
137 elif next_byte == ")":
138 brackets_stack.pop()
138 brackets_stack.pop()
139 elif next_byte == " " and not brackets_stack:
139 elif next_byte == " " and not brackets_stack:
140 break
140 break
141
141
142 return buffer_
142 return buffer_
143
143
144 def _parse_first_client_response(self, buffer_):
144 def _parse_first_client_response(self, buffer_):
145 """
145 """
146 According to the Subversion RA protocol, the first request
146 According to the Subversion RA protocol, the first request
147 should look like:
147 should look like:
148
148
149 ( version:number ( cap:word ... ) url:string ? ra-client:string
149 ( version:number ( cap:word ... ) url:string ? ra-client:string
150 ( ? client:string ) )
150 ( ? client:string ) )
151
151
152 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
152 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
153 """
153 """
154 version_re = r'(?P<version>\d+)'
154 version_re = r'(?P<version>\d+)'
155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
156 url_re = r'\d+\:(?P<url>[\W\w]+)'
156 url_re = r'\d+\:(?P<url>[\W\w]+)'
157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
159 regex = re.compile(
159 regex = re.compile(
160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
161 r'\(\s{client}\)\s\)\s*$'.format(
161 r'\(\s{client}\)\s\)\s*$'.format(
162 version=version_re, capabilities=capabilities_re,
162 version=version_re, capabilities=capabilities_re,
163 url=url_re, ra_client=ra_client_re, client=client_re))
163 url=url_re, ra_client=ra_client_re, client=client_re))
164 matcher = regex.match(buffer_)
164 matcher = regex.match(buffer_)
165
165
166 return matcher.groupdict() if matcher else None
166 return matcher.groupdict() if matcher else None
167
167
168 def _match_repo_name(self, url):
168 def _match_repo_name(self, url):
169 """
169 """
170 Given an server url, try to match it against ALL known repository names.
170 Given an server url, try to match it against ALL known repository names.
171 This handles a tricky SVN case for SSH and subdir commits.
171 This handles a tricky SVN case for SSH and subdir commits.
172 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
172 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
173 result in the url with this subdir added.
173 result in the url with this subdir added.
174 """
174 """
175 # case 1 direct match, we don't do any "heavy" lookups
175 # case 1 direct match, we don't do any "heavy" lookups
176 if url in self.server.user_permissions:
176 if url in self.server.user_permissions:
177 return url
177 return url
178
178
179 log.debug('Extracting repository name from subdir path %s', url)
179 log.debug('Extracting repository name from subdir path %s', url)
180 # case 2 we check all permissions, and match closes possible case...
180 # case 2 we check all permissions, and match closes possible case...
181 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
181 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
182 # to assume that it will have the repo name as prefix, we ensure the prefix
182 # to assume that it will have the repo name as prefix, we ensure the prefix
183 # for similar repositories isn't matched by adding a /
183 # for similar repositories isn't matched by adding a /
184 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
184 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
185 for repo_name in self.server.user_permissions:
185 for repo_name in self.server.user_permissions:
186 repo_name_prefix = repo_name + '/'
186 repo_name_prefix = repo_name + '/'
187 if url.startswith(repo_name_prefix):
187 if url.startswith(repo_name_prefix):
188 log.debug('Found prefix %s match, returning proper repository name',
188 log.debug('Found prefix %s match, returning proper repository name',
189 repo_name_prefix)
189 repo_name_prefix)
190 return repo_name
190 return repo_name
191
191
192 return
192 return
193
193
194 def run(self, extras):
194 def run(self, extras):
195 action = 'pull'
195 action = 'pull'
196 self.create_svn_config()
196 self.create_svn_config()
197 self.start()
197 self.start()
198
198
199 first_response = self.get_first_client_response()
199 first_response = self.get_first_client_response()
200 if not first_response:
200 if not first_response:
201 return self.fail("Repository name cannot be extracted")
201 return self.fail("Repository name cannot be extracted")
202
202
203 url_parts = urllib.parse.urlparse(first_response['url'])
203 url_parts = urllib.parse.urlparse(first_response['url'])
204
204
205 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
205 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
206
206
207 exit_code = self.server._check_permissions(action)
207 exit_code = self.server._check_permissions(action)
208 if exit_code:
208 if exit_code:
209 return exit_code
209 return exit_code
210
210
211 # set the readonly flag to False if we have proper permissions
211 # set the readonly flag to False if we have proper permissions
212 if self.server.has_write_perm():
212 if self.server.has_write_perm():
213 self.read_only = False
213 self.read_only = False
214 self.server.update_environment(action=action, extras=extras)
214 self.server.update_environment(action=action, extras=extras)
215
215
216 self.patch_first_client_response(first_response)
216 self.patch_first_client_response(first_response)
217 self.sync()
217 self.sync()
218 return self.return_code
218 return self.return_code
219
219
220
220
221 class SubversionServer(SSHVcsServer):
221 class SubversionServer(SshVcsServer):
222 backend = 'svn'
222 backend = 'svn'
223 repo_user_agent = 'svn'
223 repo_user_agent = 'svn'
224
224
225 def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env):
225 def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env):
226 super().__init__(user, user_permissions, settings, env)
226 super().__init__(user, user_permissions, settings, env)
227 self.store = store
227 self.store = store
228 self.ini_path = ini_path
228 self.ini_path = ini_path
229 # NOTE(dan): repo_name at this point is empty,
229 # NOTE(dan): repo_name at this point is empty,
230 # this is set later in .run() based from parsed input stream
230 # this is set later in .run() based from parsed input stream
231 self.repo_name = repo_name
231 self.repo_name = repo_name
232 self._path = self.svn_path = settings['ssh.executable.svn']
232 self._path = self.svn_path = settings['ssh.executable.svn']
233
233
234 self.tunnel = SubversionTunnelWrapper(server=self)
234 self.tunnel = SubversionTunnelWrapper(server=self)
235
235
236 def _handle_tunnel(self, extras):
236 def _handle_tunnel(self, extras):
237
237
238 # pre-auth
238 # pre-auth
239 action = 'pull'
239 action = 'pull'
240 # Special case for SVN, we extract repo name at later stage
240 # Special case for SVN, we extract repo name at later stage
241 # exit_code = self._check_permissions(action)
241 # exit_code = self._check_permissions(action)
242 # if exit_code:
242 # if exit_code:
243 # return exit_code, False
243 # return exit_code, False
244
244
245 req = self.env['request']
245 req = self.env['request']
246 server_url = req.host_url + req.script_name
246 server_url = req.host_url + req.script_name
247 extras['server_url'] = server_url
247 extras['server_url'] = server_url
248
248
249 log.debug('Using %s binaries from path %s', self.backend, self._path)
249 log.debug('Using %s binaries from path %s', self.backend, self._path)
250 exit_code = self.tunnel.run(extras)
250 exit_code = self.tunnel.run(extras)
251
251
252 return exit_code, action == "push"
252 return exit_code, action == "push"
253
253
254
254
@@ -1,38 +1,47 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 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
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
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/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
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
19 import urllib.parse
19 import urllib.parse
20
20
21 from rhodecode.lib.vcs import CurlSession
21 from rhodecode.lib.vcs import CurlSession
22 from rhodecode.lib.ext_json import json
22 from rhodecode.lib.ext_json import json
23 from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError
23
24
24
25
25 def call_service_api(service_api_host, service_api_token, api_url, payload):
26 def call_service_api(settings, payload):
26
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 )
27 payload.update({
37 payload.update({
28 'id': 'service',
38 'id': 'service',
29 'auth_token': service_api_token
39 'auth_token': api_token
30 })
40 })
31
41 service_api_url = urllib.parse.urljoin(api_host, api_url)
32 service_api_url = urllib.parse.urljoin(service_api_host, api_url)
33 response = CurlSession().post(service_api_url, json.dumps(payload))
42 response = CurlSession().post(service_api_url, json.dumps(payload))
34
43
35 if response.status_code != 200:
44 if response.status_code != 200:
36 raise Exception(f"Service API at {service_api_url} responded with error: {response.status_code}")
45 raise Exception(f"Service API at {service_api_url} responded with error: {response.status_code}")
37
46
38 return json.loads(response.content)['result']
47 return json.loads(response.content)['result']
General Comments 0
You need to be logged in to leave comments. Login now