Show More
@@ -628,14 +628,6 b' ssh.wrapper_cmd_allow_shell = false' | |||
|
628 | 628 | ## operations. Usefull for debugging, shouldn't be used in production. |
|
629 | 629 | ssh.enable_debug_logging = true |
|
630 | 630 | |
|
631 | ## API KEY for user who has access to fetch other user permission information | |
|
632 | ## most likely an super-admin account with some IP restrictions. | |
|
633 | ssh.api_key = | |
|
634 | ||
|
635 | ## API Host, the server address of RhodeCode instance that the api_key will | |
|
636 | ## access | |
|
637 | ssh.api_host = http://localhost | |
|
638 | ||
|
639 | 631 | ## Paths to binary executable, by default they are the names, but we can |
|
640 | 632 | ## override them if we want to use a custom one |
|
641 | 633 | ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg |
@@ -598,14 +598,6 b' ssh.wrapper_cmd_allow_shell = false' | |||
|
598 | 598 | ## operations. Usefull for debugging, shouldn't be used in production. |
|
599 | 599 | ssh.enable_debug_logging = false |
|
600 | 600 | |
|
601 | ## API KEY for user who has access to fetch other user permission information | |
|
602 | ## most likely an super-admin account with some IP restrictions. | |
|
603 | ssh.api_key = | |
|
604 | ||
|
605 | ## API Host, the server address of RhodeCode instance that the api_key will | |
|
606 | ## access | |
|
607 | ssh.api_host = http://localhost | |
|
608 | ||
|
609 | 601 | ## Paths to binary executable, by default they are the names, but we can |
|
610 | 602 | ## override them if we want to use a custom one |
|
611 | 603 | ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg |
@@ -45,10 +45,6 b' def _sanitize_settings_and_apply_default' | |||
|
45 | 45 | _string_setting(settings, config_keys.authorized_keys_line_ssh_opts, '', |
|
46 | 46 | lower=False) |
|
47 | 47 | |
|
48 | _string_setting(settings, config_keys.ssh_api_key, '', | |
|
49 | lower=False) | |
|
50 | _string_setting(settings, config_keys.ssh_api_host, '', | |
|
51 | lower=False) | |
|
52 | 48 | _string_setting(settings, config_keys.ssh_hg_bin, |
|
53 | 49 | '~/.rccontrol/vcsserver-1/profile/bin/hg', |
|
54 | 50 | lower=False) |
@@ -28,9 +28,6 b" wrapper_cmd = 'ssh.wrapper_cmd'" | |||
|
28 | 28 | wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell' |
|
29 | 29 | enable_debug_logging = 'ssh.enable_debug_logging' |
|
30 | 30 | |
|
31 | ssh_api_key = 'ssh.api_key' | |
|
32 | ssh_api_host = 'ssh.api_host' | |
|
33 | ||
|
34 | 31 | ssh_hg_bin = 'ssh.executable.hg' |
|
35 | 32 | ssh_git_bin = 'ssh.executable.git' |
|
36 | 33 | ssh_svn_bin = 'ssh.executable.svn' |
This diff has been collapsed as it changes many lines, (568 lines changed) Show them Hide them | |||
@@ -19,29 +19,23 b'' | |||
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import os |
|
22 | import re | |
|
23 | 22 | import sys |
|
24 | import json | |
|
25 | 23 | import logging |
|
26 | import random | |
|
27 | import signal | |
|
28 | import tempfile | |
|
29 | from subprocess import Popen, PIPE, check_output, CalledProcessError | |
|
30 | import ConfigParser | |
|
31 | import urllib2 | |
|
32 | import urlparse | |
|
33 | 24 | |
|
34 | 25 | import click |
|
35 | import pyramid.paster | |
|
36 | 26 | |
|
27 | from pyramid.paster import bootstrap, setup_logging | |
|
28 | from pyramid.request import Request | |
|
29 | ||
|
30 | from .backends import SshWrapper | |
|
37 | 31 | |
|
38 | 32 | log = logging.getLogger(__name__) |
|
39 | 33 | |
|
40 | 34 | |
|
41 | def setup_logging(ini_path, debug): | |
|
35 | def setup_custom_logging(ini_path, debug): | |
|
42 | 36 | if debug: |
|
43 | 37 | # enabled rhodecode.ini controlled logging setup |
|
44 |
|
|
|
38 | setup_logging(ini_path) | |
|
45 | 39 | else: |
|
46 | 40 | # configure logging in a mode that doesn't print anything. |
|
47 | 41 | # in case of regularly configured logging it gets printed out back |
@@ -52,532 +46,6 b' def setup_logging(ini_path, debug):' | |||
|
52 | 46 | logger.handlers = [null] |
|
53 | 47 | |
|
54 | 48 | |
|
55 | class SubversionTunnelWrapper(object): | |
|
56 | process = None | |
|
57 | ||
|
58 | def __init__(self, timeout, repositories_root=None, svn_path=None): | |
|
59 | self.timeout = timeout | |
|
60 | self.stdin = sys.stdin | |
|
61 | self.repositories_root = repositories_root | |
|
62 | self.svn_path = svn_path or 'svnserve' | |
|
63 | self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp() | |
|
64 | self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp() | |
|
65 | self.read_only = False | |
|
66 | self.create_svn_config() | |
|
67 | ||
|
68 | def create_svn_config(self): | |
|
69 | content = ( | |
|
70 | '[general]\n' | |
|
71 | 'hooks-env = {}\n').format(self.hooks_env_path) | |
|
72 | with os.fdopen(self.svn_conf_fd, 'w') as config_file: | |
|
73 | config_file.write(content) | |
|
74 | ||
|
75 | def create_hooks_env(self): | |
|
76 | content = ( | |
|
77 | '[default]\n' | |
|
78 | 'LANG = en_US.UTF-8\n') | |
|
79 | if self.read_only: | |
|
80 | content += 'SSH_READ_ONLY = 1\n' | |
|
81 | with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file: | |
|
82 | hooks_env_file.write(content) | |
|
83 | ||
|
84 | def remove_configs(self): | |
|
85 | os.remove(self.svn_conf_path) | |
|
86 | os.remove(self.hooks_env_path) | |
|
87 | ||
|
88 | def start(self): | |
|
89 | config = ['--config-file', self.svn_conf_path] | |
|
90 | command = [self.svn_path, '-t'] + config | |
|
91 | if self.repositories_root: | |
|
92 | command.extend(['-r', self.repositories_root]) | |
|
93 | self.process = Popen(command, stdin=PIPE) | |
|
94 | ||
|
95 | def sync(self): | |
|
96 | while self.process.poll() is None: | |
|
97 | next_byte = self.stdin.read(1) | |
|
98 | if not next_byte: | |
|
99 | break | |
|
100 | self.process.stdin.write(next_byte) | |
|
101 | self.remove_configs() | |
|
102 | ||
|
103 | @property | |
|
104 | def return_code(self): | |
|
105 | return self.process.returncode | |
|
106 | ||
|
107 | def get_first_client_response(self): | |
|
108 | signal.signal(signal.SIGALRM, self.interrupt) | |
|
109 | signal.alarm(self.timeout) | |
|
110 | first_response = self._read_first_client_response() | |
|
111 | signal.alarm(0) | |
|
112 | return ( | |
|
113 | self._parse_first_client_response(first_response) | |
|
114 | if first_response else None) | |
|
115 | ||
|
116 | def patch_first_client_response(self, response, **kwargs): | |
|
117 | self.create_hooks_env() | |
|
118 | data = response.copy() | |
|
119 | data.update(kwargs) | |
|
120 | data['url'] = self._svn_string(data['url']) | |
|
121 | data['ra_client'] = self._svn_string(data['ra_client']) | |
|
122 | data['client'] = data['client'] or '' | |
|
123 | buffer_ = ( | |
|
124 | "( {version} ( {capabilities} ) {url}{ra_client}" | |
|
125 | "( {client}) ) ".format(**data)) | |
|
126 | self.process.stdin.write(buffer_) | |
|
127 | ||
|
128 | def fail(self, message): | |
|
129 | print( | |
|
130 | "( failure ( ( 210005 {message} 0: 0 ) ) )".format( | |
|
131 | message=self._svn_string(message))) | |
|
132 | self.remove_configs() | |
|
133 | self.process.kill() | |
|
134 | ||
|
135 | def interrupt(self, signum, frame): | |
|
136 | self.fail("Exited by timeout") | |
|
137 | ||
|
138 | def _svn_string(self, str_): | |
|
139 | if not str_: | |
|
140 | return '' | |
|
141 | return '{length}:{string} '.format(length=len(str_), string=str_) | |
|
142 | ||
|
143 | def _read_first_client_response(self): | |
|
144 | buffer_ = "" | |
|
145 | brackets_stack = [] | |
|
146 | while True: | |
|
147 | next_byte = self.stdin.read(1) | |
|
148 | buffer_ += next_byte | |
|
149 | if next_byte == "(": | |
|
150 | brackets_stack.append(next_byte) | |
|
151 | elif next_byte == ")": | |
|
152 | brackets_stack.pop() | |
|
153 | elif next_byte == " " and not brackets_stack: | |
|
154 | break | |
|
155 | return buffer_ | |
|
156 | ||
|
157 | def _parse_first_client_response(self, buffer_): | |
|
158 | """ | |
|
159 | According to the Subversion RA protocol, the first request | |
|
160 | should look like: | |
|
161 | ||
|
162 | ( version:number ( cap:word ... ) url:string ? ra-client:string | |
|
163 | ( ? client:string ) ) | |
|
164 | ||
|
165 | Please check https://svn.apache.org/repos/asf/subversion/trunk/ | |
|
166 | subversion/libsvn_ra_svn/protocol | |
|
167 | """ | |
|
168 | version_re = r'(?P<version>\d+)' | |
|
169 | capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)' | |
|
170 | url_re = r'\d+\:(?P<url>[\W\w]+)' | |
|
171 | ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)' | |
|
172 | client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*' | |
|
173 | regex = re.compile( | |
|
174 | r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}' | |
|
175 | r'\(\s{client}\)\s\)\s*$'.format( | |
|
176 | version=version_re, capabilities=capabilities_re, | |
|
177 | url=url_re, ra_client=ra_client_re, client=client_re)) | |
|
178 | matcher = regex.match(buffer_) | |
|
179 | return matcher.groupdict() if matcher else None | |
|
180 | ||
|
181 | ||
|
182 | class RhodeCodeApiClient(object): | |
|
183 | def __init__(self, api_key, api_host): | |
|
184 | self.api_key = api_key | |
|
185 | self.api_host = api_host | |
|
186 | ||
|
187 | if not api_host: | |
|
188 | raise ValueError('api_key:{} not defined'.format(api_key)) | |
|
189 | if not api_host: | |
|
190 | raise ValueError('api_host:{} not defined '.format(api_host)) | |
|
191 | ||
|
192 | def request(self, method, args): | |
|
193 | id_ = random.randrange(1, 9999) | |
|
194 | args = { | |
|
195 | 'id': id_, | |
|
196 | 'api_key': self.api_key, | |
|
197 | 'method': method, | |
|
198 | 'args': args | |
|
199 | } | |
|
200 | host = '{host}/_admin/api'.format(host=self.api_host) | |
|
201 | ||
|
202 | log.debug('Doing API call to %s method:%s', host, method) | |
|
203 | req = urllib2.Request( | |
|
204 | host, | |
|
205 | data=json.dumps(args), | |
|
206 | headers={'content-type': 'text/plain'}) | |
|
207 | ret = urllib2.urlopen(req) | |
|
208 | raw_json = ret.read() | |
|
209 | json_data = json.loads(raw_json) | |
|
210 | id_ret = json_data['id'] | |
|
211 | ||
|
212 | if id_ret != id_: | |
|
213 | raise Exception('something went wrong. ' | |
|
214 | 'ID mismatch got %s, expected %s | %s' | |
|
215 | % (id_ret, id_, raw_json)) | |
|
216 | ||
|
217 | result = json_data['result'] | |
|
218 | error = json_data['error'] | |
|
219 | return result, error | |
|
220 | ||
|
221 | def get_user_permissions(self, user, user_id): | |
|
222 | result, error = self.request('get_user', {'userid': int(user_id)}) | |
|
223 | if result is None and error: | |
|
224 | raise Exception( | |
|
225 | 'User "%s" not found or another error happened: %s!' % ( | |
|
226 | user, error)) | |
|
227 | log.debug( | |
|
228 | 'Given User: `%s` Fetched User: `%s`', user, result.get('username')) | |
|
229 | return result.get('permissions').get('repositories') | |
|
230 | ||
|
231 | def invalidate_cache(self, repo_name): | |
|
232 | log.debug('Invalidate cache for repo:%s', repo_name) | |
|
233 | return self.request('invalidate_cache', {'repoid': repo_name}) | |
|
234 | ||
|
235 | def get_repo_store(self): | |
|
236 | result, error = self.request('get_repo_store', {}) | |
|
237 | return result | |
|
238 | ||
|
239 | ||
|
240 | class VcsServer(object): | |
|
241 | ||
|
242 | def __init__(self, user, user_permissions, config): | |
|
243 | self.user = user | |
|
244 | self.user_permissions = user_permissions | |
|
245 | self.config = config | |
|
246 | self.repo_name = None | |
|
247 | self.repo_mode = None | |
|
248 | self.store = {} | |
|
249 | self.ini_path = '' | |
|
250 | ||
|
251 | def run(self): | |
|
252 | raise NotImplementedError() | |
|
253 | ||
|
254 | def get_root_store(self): | |
|
255 | root_store = self.store['path'] | |
|
256 | if not root_store.endswith('/'): | |
|
257 | # always append trailing slash | |
|
258 | root_store = root_store + '/' | |
|
259 | return root_store | |
|
260 | ||
|
261 | ||
|
262 | class MercurialServer(VcsServer): | |
|
263 | read_only = False | |
|
264 | ||
|
265 | def __init__(self, store, ini_path, repo_name, | |
|
266 | user, user_permissions, config): | |
|
267 | super(MercurialServer, self).__init__(user, user_permissions, config) | |
|
268 | self.store = store | |
|
269 | self.repo_name = repo_name | |
|
270 | self.ini_path = ini_path | |
|
271 | self.hg_path = config.get('app:main', 'ssh.executable.hg') | |
|
272 | ||
|
273 | def run(self): | |
|
274 | if not self._check_permissions(): | |
|
275 | return 2, False | |
|
276 | ||
|
277 | tip_before = self.tip() | |
|
278 | exit_code = os.system(self.command) | |
|
279 | tip_after = self.tip() | |
|
280 | return exit_code, tip_before != tip_after | |
|
281 | ||
|
282 | def tip(self): | |
|
283 | root = self.get_root_store() | |
|
284 | command = ( | |
|
285 | 'cd {root}; {hg_path} -R {root}{repo_name} tip --template "{{node}}\n"' | |
|
286 | ''.format( | |
|
287 | root=root, hg_path=self.hg_path, repo_name=self.repo_name)) | |
|
288 | try: | |
|
289 | tip = check_output(command, shell=True).strip() | |
|
290 | except CalledProcessError: | |
|
291 | tip = None | |
|
292 | return tip | |
|
293 | ||
|
294 | @property | |
|
295 | def command(self): | |
|
296 | root = self.get_root_store() | |
|
297 | arguments = ( | |
|
298 | '--config hooks.pretxnchangegroup=\"false\"' | |
|
299 | if self.read_only else '') | |
|
300 | ||
|
301 | command = ( | |
|
302 | "cd {root}; {hg_path} -R {root}{repo_name} serve --stdio" | |
|
303 | " {arguments}".format( | |
|
304 | root=root, hg_path=self.hg_path, repo_name=self.repo_name, | |
|
305 | arguments=arguments)) | |
|
306 | log.debug("Final CMD: %s", command) | |
|
307 | return command | |
|
308 | ||
|
309 | def _check_permissions(self): | |
|
310 | permission = self.user_permissions.get(self.repo_name) | |
|
311 | if permission is None or permission == 'repository.none': | |
|
312 | log.error('repo not found or no permissions') | |
|
313 | return False | |
|
314 | ||
|
315 | elif permission in ['repository.admin', 'repository.write']: | |
|
316 | log.info( | |
|
317 | 'Write Permissions for User "%s" granted to repo "%s"!' % ( | |
|
318 | self.user, self.repo_name)) | |
|
319 | else: | |
|
320 | self.read_only = True | |
|
321 | log.info( | |
|
322 | 'Only Read Only access for User "%s" granted to repo "%s"!', | |
|
323 | self.user, self.repo_name) | |
|
324 | return True | |
|
325 | ||
|
326 | ||
|
327 | class GitServer(VcsServer): | |
|
328 | def __init__(self, store, ini_path, repo_name, repo_mode, | |
|
329 | user, user_permissions, config): | |
|
330 | super(GitServer, self).__init__(user, user_permissions, config) | |
|
331 | self.store = store | |
|
332 | self.ini_path = ini_path | |
|
333 | self.repo_name = repo_name | |
|
334 | self.repo_mode = repo_mode | |
|
335 | self.git_path = config.get('app:main', 'ssh.executable.git') | |
|
336 | ||
|
337 | def run(self): | |
|
338 | exit_code = self._check_permissions() | |
|
339 | if exit_code: | |
|
340 | return exit_code, False | |
|
341 | ||
|
342 | self._update_environment() | |
|
343 | exit_code = os.system(self.command) | |
|
344 | return exit_code, self.repo_mode == "receive-pack" | |
|
345 | ||
|
346 | @property | |
|
347 | def command(self): | |
|
348 | root = self.get_root_store() | |
|
349 | command = "cd {root}; {git_path}-{mode} '{root}{repo_name}'".format( | |
|
350 | root=root, git_path=self.git_path, mode=self.repo_mode, | |
|
351 | repo_name=self.repo_name) | |
|
352 | log.debug("Final CMD: %s", command) | |
|
353 | return command | |
|
354 | ||
|
355 | def _update_environment(self): | |
|
356 | action = "push" if self.repo_mode == "receive-pack" else "pull", | |
|
357 | scm_data = { | |
|
358 | "ip": os.environ["SSH_CLIENT"].split()[0], | |
|
359 | "username": self.user, | |
|
360 | "action": action, | |
|
361 | "repository": self.repo_name, | |
|
362 | "scm": "git", | |
|
363 | "config": self.ini_path, | |
|
364 | "make_lock": None, | |
|
365 | "locked_by": [None, None] | |
|
366 | } | |
|
367 | os.putenv("RC_SCM_DATA", json.dumps(scm_data)) | |
|
368 | ||
|
369 | def _check_permissions(self): | |
|
370 | permission = self.user_permissions.get(self.repo_name) | |
|
371 | log.debug( | |
|
372 | 'permission for %s on %s are: %s', | |
|
373 | self.user, self.repo_name, permission) | |
|
374 | ||
|
375 | if permission is None or permission == 'repository.none': | |
|
376 | log.error('repo not found or no permissions') | |
|
377 | return 2 | |
|
378 | elif permission in ['repository.admin', 'repository.write']: | |
|
379 | log.info( | |
|
380 | 'Write Permissions for User "%s" granted to repo "%s"!', | |
|
381 | self.user, self.repo_name) | |
|
382 | elif (permission == 'repository.read' and | |
|
383 | self.repo_mode == 'upload-pack'): | |
|
384 | log.info( | |
|
385 | 'Only Read Only access for User "%s" granted to repo "%s"!', | |
|
386 | self.user, self.repo_name) | |
|
387 | elif (permission == 'repository.read' | |
|
388 | and self.repo_mode == 'receive-pack'): | |
|
389 | log.error( | |
|
390 | 'Only Read Only access for User "%s" granted to repo "%s"!' | |
|
391 | ' Failing!', self.user, self.repo_name) | |
|
392 | return -3 | |
|
393 | else: | |
|
394 | log.error('Cannot properly fetch user permission. ' | |
|
395 | 'Return value is: %s', permission) | |
|
396 | return -2 | |
|
397 | ||
|
398 | ||
|
399 | class SubversionServer(VcsServer): | |
|
400 | ||
|
401 | def __init__(self, store, ini_path, | |
|
402 | user, user_permissions, config): | |
|
403 | super(SubversionServer, self).__init__(user, user_permissions, config) | |
|
404 | self.store = store | |
|
405 | self.ini_path = ini_path | |
|
406 | # this is set in .run() from input stream | |
|
407 | self.repo_name = None | |
|
408 | self.svn_path = config.get('app:main', 'ssh.executable.svn') | |
|
409 | ||
|
410 | def run(self): | |
|
411 | root = self.get_root_store() | |
|
412 | log.debug("Using subversion binaries from '%s'", self.svn_path) | |
|
413 | ||
|
414 | self.tunnel = SubversionTunnelWrapper( | |
|
415 | timeout=self.timeout, repositories_root=root, svn_path=self.svn_path) | |
|
416 | self.tunnel.start() | |
|
417 | first_response = self.tunnel.get_first_client_response() | |
|
418 | if not first_response: | |
|
419 | self.tunnel.fail("Repository name cannot be extracted") | |
|
420 | return 1, False | |
|
421 | ||
|
422 | url_parts = urlparse.urlparse(first_response['url']) | |
|
423 | self.repo_name = url_parts.path.strip('/') | |
|
424 | if not self._check_permissions(): | |
|
425 | self.tunnel.fail("Not enough permissions") | |
|
426 | return 1, False | |
|
427 | ||
|
428 | self.tunnel.patch_first_client_response(first_response) | |
|
429 | self.tunnel.sync() | |
|
430 | return self.tunnel.return_code, False | |
|
431 | ||
|
432 | @property | |
|
433 | def timeout(self): | |
|
434 | timeout = 30 | |
|
435 | return timeout | |
|
436 | ||
|
437 | def _check_permissions(self): | |
|
438 | permission = self.user_permissions.get(self.repo_name) | |
|
439 | ||
|
440 | if permission in ['repository.admin', 'repository.write']: | |
|
441 | self.tunnel.read_only = False | |
|
442 | return True | |
|
443 | ||
|
444 | elif permission == 'repository.read': | |
|
445 | self.tunnel.read_only = True | |
|
446 | return True | |
|
447 | ||
|
448 | else: | |
|
449 | self.tunnel.fail("Not enough permissions for repository {}".format( | |
|
450 | self.repo_name)) | |
|
451 | return False | |
|
452 | ||
|
453 | ||
|
454 | class SshWrapper(object): | |
|
455 | ||
|
456 | def __init__(self, command, mode, user, user_id, shell, ini_path): | |
|
457 | self.command = command | |
|
458 | self.mode = mode | |
|
459 | self.user = user | |
|
460 | self.user_id = user_id | |
|
461 | self.shell = shell | |
|
462 | self.ini_path = ini_path | |
|
463 | ||
|
464 | self.config = self.parse_config(ini_path) | |
|
465 | api_key = self.config.get('app:main', 'ssh.api_key') | |
|
466 | api_host = self.config.get('app:main', 'ssh.api_host') | |
|
467 | self.api = RhodeCodeApiClient(api_key, api_host) | |
|
468 | ||
|
469 | def parse_config(self, config): | |
|
470 | parser = ConfigParser.ConfigParser() | |
|
471 | parser.read(config) | |
|
472 | return parser | |
|
473 | ||
|
474 | def get_repo_details(self, mode): | |
|
475 | type_ = mode if mode in ['svn', 'hg', 'git'] else None | |
|
476 | mode = mode | |
|
477 | name = None | |
|
478 | ||
|
479 | hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$' | |
|
480 | hg_match = re.match(hg_pattern, self.command) | |
|
481 | if hg_match is not None: | |
|
482 | type_ = 'hg' | |
|
483 | name = hg_match.group(1).strip('/') | |
|
484 | return type_, name, mode | |
|
485 | ||
|
486 | git_pattern = ( | |
|
487 | r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$') | |
|
488 | git_match = re.match(git_pattern, self.command) | |
|
489 | if git_match is not None: | |
|
490 | type_ = 'git' | |
|
491 | name = git_match.group(2).strip('/') | |
|
492 | mode = git_match.group(1) | |
|
493 | return type_, name, mode | |
|
494 | ||
|
495 | svn_pattern = r'^svnserve -t' | |
|
496 | svn_match = re.match(svn_pattern, self.command) | |
|
497 | if svn_match is not None: | |
|
498 | type_ = 'svn' | |
|
499 | # Repo name should be extracted from the input stream | |
|
500 | return type_, name, mode | |
|
501 | ||
|
502 | return type_, name, mode | |
|
503 | ||
|
504 | def serve(self, vcs, repo, mode, user, permissions): | |
|
505 | store = self.api.get_repo_store() | |
|
506 | ||
|
507 | log.debug( | |
|
508 | 'VCS detected:`%s` mode: `%s` repo: %s', vcs, mode, repo) | |
|
509 | ||
|
510 | if vcs == 'hg': | |
|
511 | server = MercurialServer( | |
|
512 | store=store, ini_path=self.ini_path, | |
|
513 | repo_name=repo, user=user, | |
|
514 | user_permissions=permissions, config=self.config) | |
|
515 | return server.run() | |
|
516 | ||
|
517 | elif vcs == 'git': | |
|
518 | server = GitServer( | |
|
519 | store=store, ini_path=self.ini_path, | |
|
520 | repo_name=repo, repo_mode=mode, user=user, | |
|
521 | user_permissions=permissions, config=self.config) | |
|
522 | return server.run() | |
|
523 | ||
|
524 | elif vcs == 'svn': | |
|
525 | server = SubversionServer( | |
|
526 | store=store, ini_path=self.ini_path, | |
|
527 | user=user, | |
|
528 | user_permissions=permissions, config=self.config) | |
|
529 | return server.run() | |
|
530 | ||
|
531 | else: | |
|
532 | raise Exception('Unrecognised VCS: {}'.format(vcs)) | |
|
533 | ||
|
534 | def wrap(self): | |
|
535 | mode = self.mode | |
|
536 | user = self.user | |
|
537 | user_id = self.user_id | |
|
538 | shell = self.shell | |
|
539 | ||
|
540 | scm_detected, scm_repo, scm_mode = self.get_repo_details(mode) | |
|
541 | log.debug( | |
|
542 | 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` ' | |
|
543 | 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`', | |
|
544 | mode, user, user_id, shell, self.command, | |
|
545 | scm_detected, scm_mode, scm_repo) | |
|
546 | ||
|
547 | try: | |
|
548 | permissions = self.api.get_user_permissions(user, user_id) | |
|
549 | except Exception as e: | |
|
550 | log.exception('Failed to fetch user permissions') | |
|
551 | return 1 | |
|
552 | ||
|
553 | if shell and self.command is None: | |
|
554 | log.info( | |
|
555 | 'Dropping to shell, no command given and shell is allowed') | |
|
556 | os.execl('/bin/bash', '-l') | |
|
557 | exit_code = 1 | |
|
558 | ||
|
559 | elif scm_detected: | |
|
560 | try: | |
|
561 | exit_code, is_updated = self.serve( | |
|
562 | scm_detected, scm_repo, scm_mode, user, permissions) | |
|
563 | if exit_code == 0 and is_updated: | |
|
564 | self.api.invalidate_cache(scm_repo) | |
|
565 | except Exception: | |
|
566 | log.exception('Error occurred during execution of SshWrapper') | |
|
567 | exit_code = -1 | |
|
568 | ||
|
569 | elif self.command is None and shell is False: | |
|
570 | log.error('No Command given.') | |
|
571 | exit_code = -1 | |
|
572 | ||
|
573 | else: | |
|
574 | log.error( | |
|
575 | 'Unhandled Command: "%s" Aborting.', self.command) | |
|
576 | exit_code = -1 | |
|
577 | ||
|
578 | return exit_code | |
|
579 | ||
|
580 | ||
|
581 | 49 | @click.command() |
|
582 | 50 | @click.argument('ini_path', type=click.Path(exists=True)) |
|
583 | 51 | @click.option( |
@@ -586,10 +54,11 b' class SshWrapper(object):' | |||
|
586 | 54 | help='mode of operation') |
|
587 | 55 | @click.option('--user', help='Username for which the command will be executed') |
|
588 | 56 | @click.option('--user-id', help='User ID for which the command will be executed') |
|
57 | @click.option('--key-id', help='ID of the key from the database') | |
|
589 | 58 | @click.option('--shell', '-s', is_flag=True, help='Allow Shell') |
|
590 | 59 | @click.option('--debug', is_flag=True, help='Enabled detailed output logging') |
|
591 | def main(ini_path, mode, user, user_id, shell, debug): | |
|
592 | setup_logging(ini_path, debug) | |
|
60 | def main(ini_path, mode, user, user_id, key_id, shell, debug): | |
|
61 | setup_custom_logging(ini_path, debug) | |
|
593 | 62 | |
|
594 | 63 | command = os.environ.get('SSH_ORIGINAL_COMMAND', '') |
|
595 | 64 | if not command and mode not in ['test']: |
@@ -597,11 +66,16 b' def main(ini_path, mode, user, user_id, ' | |||
|
597 | 66 | 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.' |
|
598 | 67 | 'Please make sure this is set and available during execution ' |
|
599 | 68 | 'of this script.') |
|
69 | connection_info = os.environ.get('SSH_CONNECTION', '') | |
|
70 | request = Request.blank('/', base_url='http://rhodecode-ssh-wrapper/') | |
|
71 | with bootstrap(ini_path, request=request) as env: | |
|
72 | try: | |
|
73 | ssh_wrapper = SshWrapper( | |
|
74 | command, connection_info, mode, | |
|
75 | user, user_id, key_id, shell, ini_path) | |
|
76 | except Exception: | |
|
77 | log.exception('Failed to execute SshWrapper') | |
|
78 | sys.exit(-5) | |
|
600 | 79 | |
|
601 | try: | |
|
602 | ssh_wrapper = SshWrapper(command, mode, user, user_id, shell, ini_path) | |
|
603 | except Exception: | |
|
604 | log.exception('Failed to execute SshWrapper') | |
|
605 | sys.exit(-5) | |
|
606 | ||
|
607 | sys.exit(ssh_wrapper.wrap()) No newline at end of file | |
|
80 | return_code = ssh_wrapper.wrap() | |
|
81 | sys.exit(return_code) |
@@ -21,9 +21,9 b'' | |||
|
21 | 21 | import json |
|
22 | 22 | |
|
23 | 23 | import pytest |
|
24 |
from mock import Mock, patch |
|
|
24 | from mock import Mock, patch | |
|
25 | 25 | |
|
26 |
from rhodecode.apps.ssh_support.lib. |
|
|
26 | from rhodecode.apps.ssh_support.lib.backends.git import GitServer | |
|
27 | 27 | |
|
28 | 28 | |
|
29 | 29 | @pytest.fixture |
@@ -19,9 +19,9 b'' | |||
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import pytest |
|
22 |
from mock import Mock, patch |
|
|
22 | from mock import Mock, patch | |
|
23 | 23 | |
|
24 |
from rhodecode.apps.ssh_support.lib. |
|
|
24 | from rhodecode.apps.ssh_support.lib.backends.hg import MercurialServer | |
|
25 | 25 | |
|
26 | 26 | |
|
27 | 27 | @pytest.fixture |
@@ -19,9 +19,9 b'' | |||
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import pytest |
|
22 |
from mock import Mock, patch |
|
|
22 | from mock import Mock, patch | |
|
23 | 23 | |
|
24 |
from rhodecode.apps.ssh_support.lib. |
|
|
24 | from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer | |
|
25 | 25 | |
|
26 | 26 | |
|
27 | 27 | @pytest.fixture |
@@ -18,8 +18,6 b'' | |||
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | ||
|
22 | ||
|
23 | 21 | import os |
|
24 | 22 | import pytest |
|
25 | 23 | import mock |
@@ -33,9 +33,6 b' def dummy_conf(tmpdir):' | |||
|
33 | 33 | conf.set('app:main', 'ssh.executable.git', '/usr/bin/git') |
|
34 | 34 | conf.set('app:main', 'ssh.executable.svn', '/usr/bin/svnserve') |
|
35 | 35 | |
|
36 | conf.set('app:main', 'ssh.api_key', 'xxx') | |
|
37 | conf.set('app:main', 'ssh.api_host', 'http://localhost') | |
|
38 | ||
|
39 | 36 | f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini') |
|
40 | 37 | with open(f_path, 'wb') as f: |
|
41 | 38 | conf.write(f) |
@@ -26,7 +26,7 b' from time import sleep' | |||
|
26 | 26 | import pytest |
|
27 | 27 | from mock import patch, Mock, MagicMock, call |
|
28 | 28 | |
|
29 |
from rhodecode.apps.ssh_support.lib. |
|
|
29 | from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper | |
|
30 | 30 | from rhodecode.tests import no_newline_id_generator |
|
31 | 31 | |
|
32 | 32 |
@@ -66,7 +66,7 b' def _generate_ssh_authorized_keys_file(' | |||
|
66 | 66 | raise OSError('Access to file {} is without read access'.format( |
|
67 | 67 | authorized_keys_file_path)) |
|
68 | 68 | |
|
69 | line_tmpl = '{ssh_opts},command="{wrapper_command} {ini_path} --user-id={user_id} --user={user}" {key}\n' | |
|
69 | line_tmpl = '{ssh_opts},command="{wrapper_command} {ini_path} --user-id={user_id} --user={user} --key-id={user_key_id}" {key}\n' | |
|
70 | 70 | |
|
71 | 71 | fd, tmp_authorized_keys = tempfile.mkstemp( |
|
72 | 72 | '.authorized_keys_write', |
@@ -87,7 +87,9 b' def _generate_ssh_authorized_keys_file(' | |||
|
87 | 87 | wrapper_command=ssh_wrapper_cmd, |
|
88 | 88 | ini_path=ini_path, |
|
89 | 89 | user_id=user_id, |
|
90 |
user=username, |
|
|
90 | user=username, | |
|
91 | user_key_id=user_key.ssh_key_id, | |
|
92 | key=user_key.ssh_key_data)) | |
|
91 | 93 | log.debug('addkey: Key added for user: `%s`', username) |
|
92 | 94 | keys_file.close() |
|
93 | 95 |
@@ -664,14 +664,6 b' ssh.wrapper_cmd_allow_shell = false' | |||
|
664 | 664 | ## debugging, shouldn't be used in production. |
|
665 | 665 | ssh.enable_debug_logging = false |
|
666 | 666 | |
|
667 | ## API KEY for user who has access to fetch other user permission information | |
|
668 | ## most likely an super-admin account with some IP restrictions. | |
|
669 | ssh.api_key = | |
|
670 | ||
|
671 | ## API Host, the server address of RhodeCode instance that the api_key will | |
|
672 | ## access | |
|
673 | ssh.api_host = http://localhost | |
|
674 | ||
|
675 | 667 | ## Paths to binary executrables, by default they are the names, but we can |
|
676 | 668 | ## override them if we want to use a custom one |
|
677 | 669 | ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg |
General Comments 0
You need to be logged in to leave comments.
Login now