##// END OF EJS Templates
ssh: embedded ssh support...
marcink -
r2043:338dc54d default
parent child Browse files
Show More
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
This diff has been collapsed as it changes many lines, (607 lines changed) Show them Hide them
@@ -0,0 +1,607 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import re
23 import sys
24 import json
25 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
34 import click
35 import pyramid.paster
36
37
38 log = logging.getLogger(__name__)
39
40
41 def setup_logging(ini_path, debug):
42 if debug:
43 # enabled rhodecode.ini controlled logging setup
44 pyramid.paster.setup_logging(ini_path)
45 else:
46 # configure logging in a mode that doesn't print anything.
47 # in case of regularly configured logging it gets printed out back
48 # to the client doing an SSH command.
49 logger = logging.getLogger('')
50 null = logging.NullHandler()
51 # add the handler to the root logger
52 logger.handlers = [null]
53
54
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 @click.command()
582 @click.argument('ini_path', type=click.Path(exists=True))
583 @click.option(
584 '--mode', '-m', required=False, default='auto',
585 type=click.Choice(['auto', 'vcs', 'git', 'hg', 'svn', 'test']),
586 help='mode of operation')
587 @click.option('--user', help='Username for which the command will be executed')
588 @click.option('--user-id', help='User ID for which the command will be executed')
589 @click.option('--shell', '-s', is_flag=True, help='Allow Shell')
590 @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)
593
594 command = os.environ.get('SSH_ORIGINAL_COMMAND', '')
595 if not command and mode not in ['test']:
596 raise ValueError(
597 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.'
598 'Please make sure this is set and available during execution '
599 'of this script.')
600
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
@@ -0,0 +1,195 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import json
22
23 import pytest
24 from mock import Mock, patch, call
25
26 from rhodecode.apps.ssh_support.lib.ssh_wrapper import GitServer
27
28
29 @pytest.fixture
30 def git_server():
31 return GitServerCreator()
32
33
34 class GitServerCreator(object):
35 root = '/tmp/repo/path/'
36 git_path = '/usr/local/bin/'
37 config_data = {
38 'app:main': {
39 'ssh.executable.git': git_path
40 }
41 }
42 repo_name = 'test_git'
43 repo_mode = 'receive-pack'
44 user = 'vcs'
45
46 def __init__(self):
47 def config_get(part, key):
48 return self.config_data.get(part, {}).get(key)
49 self.config_mock = Mock()
50 self.config_mock.get = Mock(side_effect=config_get)
51
52 def create(self, **kwargs):
53 parameters = {
54 'store': {'path': self.root},
55 'ini_path': '',
56 'user': self.user,
57 'repo_name': self.repo_name,
58 'repo_mode': self.repo_mode,
59 'user_permissions': {
60 self.repo_name: 'repo_admin'
61 },
62 'config': self.config_mock,
63 }
64 parameters.update(kwargs)
65 server = GitServer(**parameters)
66 return server
67
68
69 class TestGitServer(object):
70 def test_command(self, git_server):
71 server = git_server.create()
72 server.read_only = False
73 expected_command = (
74 'cd {root}; {git_path}-{repo_mode}'
75 ' \'{root}{repo_name}\''.format(
76 root=git_server.root, git_path=git_server.git_path,
77 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
78 )
79 assert expected_command == server.command
80
81 def test_run_returns_exit_code_2_when_no_permissions(self, git_server, caplog):
82 server = git_server.create()
83 with patch.object(server, '_check_permissions') as permissions_mock:
84 with patch.object(server, '_update_environment'):
85 permissions_mock.return_value = 2
86 exit_code = server.run()
87
88 assert exit_code == (2, False)
89
90 def test_run_returns_executes_command(self, git_server, caplog):
91 server = git_server.create()
92 with patch.object(server, '_check_permissions') as permissions_mock:
93 with patch('os.system') as system_mock:
94 with patch.object(server, '_update_environment') as (
95 update_mock):
96 permissions_mock.return_value = 0
97 system_mock.return_value = 0
98 exit_code = server.run()
99
100 system_mock.assert_called_once_with(server.command)
101 update_mock.assert_called_once_with()
102
103 assert exit_code == (0, True)
104
105 @pytest.mark.parametrize(
106 'repo_mode, action', [
107 ['receive-pack', 'push'],
108 ['upload-pack', 'pull']
109 ])
110 def test_update_environment(self, git_server, repo_mode, action):
111 server = git_server.create(repo_mode=repo_mode)
112 with patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
113 with patch('os.putenv') as putenv_mock:
114 server._update_environment()
115 expected_data = {
116 "username": git_server.user,
117 "scm": "git",
118 "repository": git_server.repo_name,
119 "make_lock": None,
120 "action": [action],
121 "ip": "10.10.10.10",
122 "locked_by": [None, None],
123 "config": ""
124 }
125 putenv_mock.assert_called_once_with(
126 'RC_SCM_DATA', json.dumps(expected_data))
127
128
129 class TestGitServerCheckPermissions(object):
130 def test_returns_2_when_no_permissions_found(self, git_server, caplog):
131 user_permissions = {}
132 server = git_server.create(user_permissions=user_permissions)
133 result = server._check_permissions()
134 assert result == 2
135
136 log_msg = 'permission for vcs on test_git are: None'
137 assert log_msg in [t[2] for t in caplog.record_tuples]
138
139 def test_returns_2_when_no_permissions(self, git_server, caplog):
140 user_permissions = {git_server.repo_name: 'repository.none'}
141 server = git_server.create(user_permissions=user_permissions)
142 result = server._check_permissions()
143 assert result == 2
144
145 log_msg = 'repo not found or no permissions'
146 assert log_msg in [t[2] for t in caplog.record_tuples]
147
148 @pytest.mark.parametrize(
149 'permission', ['repository.admin', 'repository.write'])
150 def test_access_allowed_when_user_has_write_permissions(
151 self, git_server, permission, caplog):
152 user_permissions = {git_server.repo_name: permission}
153 server = git_server.create(user_permissions=user_permissions)
154 result = server._check_permissions()
155 assert result is None
156
157 log_msg = 'Write Permissions for User "%s" granted to repo "%s"!' % (
158 git_server.user, git_server.repo_name)
159 assert log_msg in [t[2] for t in caplog.record_tuples]
160
161 def test_write_access_is_not_allowed_when_user_has_read_permission(
162 self, git_server, caplog):
163 user_permissions = {git_server.repo_name: 'repository.read'}
164 server = git_server.create(
165 user_permissions=user_permissions, repo_mode='receive-pack')
166 result = server._check_permissions()
167 assert result == -3
168
169 log_msg = 'Only Read Only access for User "%s" granted to repo "%s"! Failing!' % (
170 git_server.user, git_server.repo_name)
171 assert log_msg in [t[2] for t in caplog.record_tuples]
172
173 def test_read_access_allowed_when_user_has_read_permission(
174 self, git_server, caplog):
175 user_permissions = {git_server.repo_name: 'repository.read'}
176 server = git_server.create(
177 user_permissions=user_permissions, repo_mode='upload-pack')
178 result = server._check_permissions()
179 assert result is None
180
181 log_msg = 'Only Read Only access for User "%s" granted to repo "%s"!' % (
182 git_server.user, git_server.repo_name)
183 assert log_msg in [t[2] for t in caplog.record_tuples]
184
185 def test_returns_error_when_permission_not_recognised(
186 self, git_server, caplog):
187 user_permissions = {git_server.repo_name: 'repository.whatever'}
188 server = git_server.create(
189 user_permissions=user_permissions, repo_mode='upload-pack')
190 result = server._check_permissions()
191 assert result == -2
192
193 log_msg = 'Cannot properly fetch user permission. ' \
194 'Return value is: repository.whatever'
195 assert log_msg in [t[2] for t in caplog.record_tuples] No newline at end of file
@@ -0,0 +1,146 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22 from mock import Mock, patch, call
23
24 from rhodecode.apps.ssh_support.lib.ssh_wrapper import MercurialServer
25
26
27 @pytest.fixture
28 def hg_server():
29 return MercurialServerCreator()
30
31
32 class MercurialServerCreator(object):
33 root = '/tmp/repo/path/'
34 hg_path = '/usr/local/bin/hg'
35
36 config_data = {
37 'app:main': {
38 'ssh.executable.hg': hg_path
39 }
40 }
41 repo_name = 'test_hg'
42 user = 'vcs'
43
44 def __init__(self):
45 def config_get(part, key):
46 return self.config_data.get(part, {}).get(key)
47 self.config_mock = Mock()
48 self.config_mock.get = Mock(side_effect=config_get)
49
50 def create(self, **kwargs):
51 parameters = {
52 'store': {'path': self.root},
53 'ini_path': '',
54 'user': self.user,
55 'repo_name': self.repo_name,
56 'user_permissions': {
57 'test_hg': 'repo_admin'
58 },
59 'config': self.config_mock,
60 }
61 parameters.update(kwargs)
62 server = MercurialServer(**parameters)
63 return server
64
65
66 class TestMercurialServer(object):
67 def test_read_only_command(self, hg_server):
68 server = hg_server.create()
69 server.read_only = True
70 expected_command = (
71 'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio'
72 ' --config hooks.pretxnchangegroup="false"'.format(
73 root=hg_server.root, hg_path=hg_server.hg_path,
74 repo_name=hg_server.repo_name)
75 )
76 assert expected_command == server.command
77
78 def test_normal_command(self, hg_server):
79 server = hg_server.create()
80 server.read_only = False
81 expected_command = (
82 'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio '.format(
83 root=hg_server.root, hg_path=hg_server.hg_path,
84 repo_name=hg_server.repo_name)
85 )
86 assert expected_command == server.command
87
88 def test_access_rejected_when_permissions_are_not_found(self, hg_server, caplog):
89 user_permissions = {}
90 server = hg_server.create(user_permissions=user_permissions)
91 result = server._check_permissions()
92 assert result is False
93
94 log_msg = 'repo not found or no permissions'
95 assert log_msg in [t[2] for t in caplog.record_tuples]
96
97 def test_access_rejected_when_no_permissions(self, hg_server, caplog):
98 user_permissions = {hg_server.repo_name: 'repository.none'}
99 server = hg_server.create(user_permissions=user_permissions)
100 result = server._check_permissions()
101 assert result is False
102
103 log_msg = 'repo not found or no permissions'
104 assert log_msg in [t[2] for t in caplog.record_tuples]
105
106 @pytest.mark.parametrize(
107 'permission', ['repository.admin', 'repository.write'])
108 def test_access_allowed_when_user_has_write_permissions(
109 self, hg_server, permission, caplog):
110 user_permissions = {hg_server.repo_name: permission}
111 server = hg_server.create(user_permissions=user_permissions)
112 result = server._check_permissions()
113 assert result is True
114
115 assert server.read_only is False
116 log_msg = 'Write Permissions for User "vcs" granted to repo "test_hg"!'
117 assert log_msg in [t[2] for t in caplog.record_tuples]
118
119 def test_access_allowed_when_user_has_read_permissions(self, hg_server, caplog):
120 user_permissions = {hg_server.repo_name: 'repository.read'}
121 server = hg_server.create(user_permissions=user_permissions)
122 result = server._check_permissions()
123 assert result is True
124
125 assert server.read_only is True
126 log_msg = 'Only Read Only access for User "%s" granted to repo "%s"!' % (
127 hg_server.user, hg_server.repo_name)
128 assert log_msg in [t[2] for t in caplog.record_tuples]
129
130 def test_run_returns_exit_code_2_when_no_permissions(self, hg_server, caplog):
131 server = hg_server.create()
132 with patch.object(server, '_check_permissions') as permissions_mock:
133 permissions_mock.return_value = False
134 exit_code = server.run()
135 assert exit_code == (2, False)
136
137 def test_run_returns_executes_command(self, hg_server, caplog):
138 server = hg_server.create()
139 with patch.object(server, '_check_permissions') as permissions_mock:
140 with patch('os.system') as system_mock:
141 permissions_mock.return_value = True
142 system_mock.return_value = 0
143 exit_code = server.run()
144
145 system_mock.assert_called_once_with(server.command)
146 assert exit_code == (0, False)
@@ -0,0 +1,136 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22 from mock import Mock, patch, call
23
24 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SubversionServer
25
26
27 @pytest.fixture
28 def svn_server():
29 return SubversionServerCreator()
30
31
32 class SubversionServerCreator(object):
33 root = '/tmp/repo/path/'
34 svn_path = '/usr/local/bin/svnserve'
35 config_data = {
36 'app:main': {
37 'ssh.executable.svn': svn_path
38 }
39 }
40 repo_name = 'test-svn'
41 user = 'vcs'
42
43 def __init__(self):
44 def config_get(part, key):
45 return self.config_data.get(part, {}).get(key)
46 self.config_mock = Mock()
47 self.config_mock.get = Mock(side_effect=config_get)
48
49 def create(self, **kwargs):
50 parameters = {
51 'store': {'path': self.root},
52 'ini_path': '',
53 'user': self.user,
54 'user_permissions': {
55 self.repo_name: 'repo_admin'
56 },
57 'config': self.config_mock,
58 }
59 parameters.update(kwargs)
60 server = SubversionServer(**parameters)
61 return server
62
63
64 class TestSubversionServer(object):
65 def test_timeout_returns_value_from_config(self, svn_server):
66 server = svn_server.create()
67 assert server.timeout == 30
68
69 @pytest.mark.parametrize(
70 'permission', ['repository.admin', 'repository.write'])
71 def test_check_permissions_with_write_permissions(
72 self, svn_server, permission):
73 user_permissions = {svn_server.repo_name: permission}
74 server = svn_server.create(user_permissions=user_permissions)
75 server.tunnel = Mock()
76 server.repo_name = svn_server.repo_name
77 result = server._check_permissions()
78 assert result is True
79 assert server.tunnel.read_only is False
80
81 def test_check_permissions_with_read_permissions(self, svn_server):
82 user_permissions = {svn_server.repo_name: 'repository.read'}
83 server = svn_server.create(user_permissions=user_permissions)
84 server.tunnel = Mock()
85 server.repo_name = svn_server.repo_name
86 result = server._check_permissions()
87 assert result is True
88 assert server.tunnel.read_only is True
89
90 def test_check_permissions_with_no_permissions(self, svn_server, caplog):
91 tunnel_mock = Mock()
92 user_permissions = {}
93 server = svn_server.create(user_permissions=user_permissions)
94 server.tunnel = tunnel_mock
95 server.repo_name = svn_server.repo_name
96 result = server._check_permissions()
97 assert result is False
98 tunnel_mock.fail.assert_called_once_with(
99 "Not enough permissions for repository {}".format(
100 svn_server.repo_name))
101
102 def test_run_returns_1_when_repository_name_cannot_be_extracted(
103 self, svn_server):
104 server = svn_server.create()
105 with patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.SubversionTunnelWrapper') as tunnel_mock:
106 tunnel_mock().get_first_client_response.return_value = None
107 exit_code = server.run()
108 assert exit_code == (1, False)
109 tunnel_mock().fail.assert_called_once_with(
110 'Repository name cannot be extracted')
111
112 def test_run_returns_tunnel_return_code(self, svn_server, caplog):
113 server = svn_server.create()
114 fake_response = {
115 'url': 'ssh+svn://test@example.com/test-svn/'
116 }
117 with patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.SubversionTunnelWrapper') as tunnel_mock:
118 with patch.object(server, '_check_permissions') as (
119 permissions_mock):
120 permissions_mock.return_value = True
121 tunnel = tunnel_mock()
122 tunnel.get_first_client_response.return_value = fake_response
123 tunnel.return_code = 0
124 exit_code = server.run()
125 permissions_mock.assert_called_once_with()
126
127 expected_log_calls = sorted([
128 "Using subversion binaries from '%s'" % svn_server.svn_path
129 ])
130
131 assert expected_log_calls == [t[2] for t in caplog.record_tuples]
132
133 assert exit_code == (0, False)
134 tunnel.patch_first_client_response.assert_called_once_with(
135 fake_response)
136 tunnel.sync.assert_called_once_with()
@@ -0,0 +1,204 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
21 import mock
22 import pytest
23 import ConfigParser
24
25 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper
26
27
28 @pytest.fixture
29 def dummy_conf(tmpdir):
30 conf = ConfigParser.ConfigParser()
31 conf.add_section('app:main')
32 conf.set('app:main', 'ssh.executable.hg', '/usr/bin/hg')
33 conf.set('app:main', 'ssh.executable.git', '/usr/bin/git')
34 conf.set('app:main', 'ssh.executable.svn', '/usr/bin/svnserve')
35
36 conf.set('app:main', 'ssh.api_key', 'xxx')
37 conf.set('app:main', 'ssh.api_host', 'http://localhost')
38
39 f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini')
40 with open(f_path, 'wb') as f:
41 conf.write(f)
42
43 return os.path.join(f_path)
44
45
46 class TestGetRepoDetails(object):
47 @pytest.mark.parametrize(
48 'command', [
49 'hg -R test-repo serve --stdio',
50 'hg -R test-repo serve --stdio'
51 ])
52 def test_hg_command_matched(self, command, dummy_conf):
53 wrapper = SshWrapper(command, 'auto', 'admin', '3', 'False', dummy_conf)
54 type_, name, mode = wrapper.get_repo_details('auto')
55 assert type_ == 'hg'
56 assert name == 'test-repo'
57 assert mode is 'auto'
58
59 @pytest.mark.parametrize(
60 'command', [
61 'hg test-repo serve --stdio',
62 'hg -R test-repo serve',
63 'hg serve --stdio',
64 'hg serve -R test-repo'
65 ])
66 def test_hg_command_not_matched(self, command, dummy_conf):
67 wrapper = SshWrapper(command, 'auto', 'admin', '3', 'False', dummy_conf)
68 type_, name, mode = wrapper.get_repo_details('auto')
69 assert type_ is None
70 assert name is None
71 assert mode is 'auto'
72
73
74 class TestServe(object):
75 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, dummy_conf):
76 with mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store'):
77 wrapper = SshWrapper('random command', 'auto', 'admin', '3', 'False', dummy_conf)
78
79 with pytest.raises(Exception) as exc_info:
80 wrapper.serve(
81 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
82 permissions={})
83 assert exc_info.value.message == 'Unrecognised VCS: microsoft-tfs'
84
85
86 class TestServeHg(object):
87
88 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
89 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
90 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
91 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.MercurialServer.run')
92 def test_serve_creates_hg_instance(
93 self, mercurial_run_mock, get_repo_store_mock, get_user_mock,
94 invalidate_cache_mock, dummy_conf):
95
96 repo_name = None
97 mercurial_run_mock.return_value = 0, True
98 get_user_mock.return_value = {repo_name: 'repository.admin'}
99 get_repo_store_mock.return_value = {'path': '/tmp'}
100
101 wrapper = SshWrapper('date', 'hg', 'admin', '3', 'False',
102 dummy_conf)
103 exit_code = wrapper.wrap()
104 assert exit_code == 0
105 assert mercurial_run_mock.called
106
107 assert get_repo_store_mock.called
108 assert get_user_mock.called
109 invalidate_cache_mock.assert_called_once_with(repo_name)
110
111 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
112 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
113 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
114 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.MercurialServer.run')
115 def test_serve_hg_invalidates_cache(
116 self, mercurial_run_mock, get_repo_store_mock, get_user_mock,
117 invalidate_cache_mock, dummy_conf):
118
119 repo_name = None
120 mercurial_run_mock.return_value = 0, True
121 get_user_mock.return_value = {repo_name: 'repository.admin'}
122 get_repo_store_mock.return_value = {'path': '/tmp'}
123
124 wrapper = SshWrapper('date', 'hg', 'admin', '3', 'False',
125 dummy_conf)
126 exit_code = wrapper.wrap()
127 assert exit_code == 0
128 assert mercurial_run_mock.called
129
130 assert get_repo_store_mock.called
131 assert get_user_mock.called
132 invalidate_cache_mock.assert_called_once_with(repo_name)
133
134
135 class TestServeGit(object):
136
137 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
138 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
139 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
140 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.GitServer.run')
141 def test_serve_creates_git_instance(self, git_run_mock, get_repo_store_mock, get_user_mock,
142 invalidate_cache_mock, dummy_conf):
143 repo_name = None
144 git_run_mock.return_value = 0, True
145 get_user_mock.return_value = {repo_name: 'repository.admin'}
146 get_repo_store_mock.return_value = {'path': '/tmp'}
147
148 wrapper = SshWrapper('date', 'git', 'admin', '3', 'False',
149 dummy_conf)
150
151 exit_code = wrapper.wrap()
152 assert exit_code == 0
153 assert git_run_mock.called
154 assert get_repo_store_mock.called
155 assert get_user_mock.called
156 invalidate_cache_mock.assert_called_once_with(repo_name)
157
158 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
159 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
160 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
161 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.GitServer.run')
162 def test_serve_git_invalidates_cache(
163 self, git_run_mock, get_repo_store_mock, get_user_mock,
164 invalidate_cache_mock, dummy_conf):
165 repo_name = None
166 git_run_mock.return_value = 0, True
167 get_user_mock.return_value = {repo_name: 'repository.admin'}
168 get_repo_store_mock.return_value = {'path': '/tmp'}
169
170 wrapper = SshWrapper('date', 'git', 'admin', '3', 'False', dummy_conf)
171
172 exit_code = wrapper.wrap()
173 assert exit_code == 0
174 assert git_run_mock.called
175
176 assert get_repo_store_mock.called
177 assert get_user_mock.called
178 invalidate_cache_mock.assert_called_once_with(repo_name)
179
180
181 class TestServeSvn(object):
182
183 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
184 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
185 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
186 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.SubversionServer.run')
187 def test_serve_creates_svn_instance(
188 self, svn_run_mock, get_repo_store_mock, get_user_mock,
189 invalidate_cache_mock, dummy_conf):
190
191 repo_name = None
192 svn_run_mock.return_value = 0, True
193 get_user_mock.return_value = {repo_name: 'repository.admin'}
194 get_repo_store_mock.return_value = {'path': '/tmp'}
195
196 wrapper = SshWrapper('date', 'svn', 'admin', '3', 'False', dummy_conf)
197
198 exit_code = wrapper.wrap()
199 assert exit_code == 0
200 assert svn_run_mock.called
201
202 assert get_repo_store_mock.called
203 assert get_user_mock.called
204 invalidate_cache_mock.assert_called_once_with(repo_name)
@@ -0,0 +1,285 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import subprocess
23 from io import BytesIO
24 from time import sleep
25
26 import pytest
27 from mock import patch, Mock, MagicMock, call
28
29 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SubversionTunnelWrapper
30 from rhodecode.tests import no_newline_id_generator
31
32
33 class TestSubversionTunnelWrapper(object):
34 @pytest.mark.parametrize(
35 'input_string, output_string', [
36 [None, ''],
37 ['abcde', '5:abcde '],
38 ['abcdefghijk', '11:abcdefghijk ']
39 ])
40 def test_svn_string(self, input_string, output_string):
41 wrapper = SubversionTunnelWrapper(timeout=5)
42 assert wrapper._svn_string(input_string) == output_string
43
44 def test_read_first_client_response(self):
45 wrapper = SubversionTunnelWrapper(timeout=5)
46 buffer_ = '( abcd ( efg hij ) ) '
47 wrapper.stdin = BytesIO(buffer_)
48 result = wrapper._read_first_client_response()
49 assert result == buffer_
50
51 def test_parse_first_client_response_returns_dict(self):
52 response = (
53 '( 2 ( edit-pipeline svndiff1 absent-entries depth mergeinfo'
54 ' log-revprops ) 26:svn+ssh://vcs@vm/hello-svn 38:SVN/1.8.11'
55 ' (x86_64-apple-darwin14.1.0) ( ) ) ')
56 wrapper = SubversionTunnelWrapper(timeout=5)
57 result = wrapper._parse_first_client_response(response)
58 assert result['version'] == '2'
59 assert (
60 result['capabilities'] ==
61 'edit-pipeline svndiff1 absent-entries depth mergeinfo'
62 ' log-revprops')
63 assert result['url'] == 'svn+ssh://vcs@vm/hello-svn'
64 assert result['ra_client'] == 'SVN/1.8.11 (x86_64-apple-darwin14.1.0)'
65 assert result['client'] is None
66
67 def test_parse_first_client_response_returns_none_when_not_matched(self):
68 response = (
69 '( 2 ( edit-pipeline svndiff1 absent-entries depth mergeinfo'
70 ' log-revprops ) ) ')
71 wrapper = SubversionTunnelWrapper(timeout=5)
72 result = wrapper._parse_first_client_response(response)
73 assert result is None
74
75 def test_interrupt(self):
76 wrapper = SubversionTunnelWrapper(timeout=5)
77 with patch.object(wrapper, 'fail') as fail_mock:
78 wrapper.interrupt(1, 'frame')
79 fail_mock.assert_called_once_with("Exited by timeout")
80
81 def test_fail(self):
82 process_mock = Mock()
83 wrapper = SubversionTunnelWrapper(timeout=5)
84 with patch.object(wrapper, 'remove_configs') as remove_configs_mock:
85 with patch('sys.stdout', new_callable=BytesIO) as stdout_mock:
86 with patch.object(wrapper, 'process') as process_mock:
87 wrapper.fail('test message')
88 assert (
89 stdout_mock.getvalue() ==
90 '( failure ( ( 210005 12:test message 0: 0 ) ) )\n')
91 process_mock.kill.assert_called_once_with()
92 remove_configs_mock.assert_called_once_with()
93
94 @pytest.mark.parametrize(
95 'client, expected_client', [
96 ['test ', 'test '],
97 ['', ''],
98 [None, '']
99 ])
100 def test_client_in_patch_first_client_response(
101 self, client, expected_client):
102 response = {
103 'version': 2,
104 'capabilities': 'edit-pipeline svndiff1 absent-entries depth',
105 'url': 'svn+ssh://example.com/svn',
106 'ra_client': 'SVN/1.8.11 (x86_64-apple-darwin14.1.0)',
107 'client': client
108 }
109 wrapper = SubversionTunnelWrapper(timeout=5)
110 stdin = BytesIO()
111 with patch.object(wrapper, 'process') as process_mock:
112 process_mock.stdin = stdin
113 wrapper.patch_first_client_response(response)
114 assert (
115 stdin.getvalue() ==
116 '( 2 ( edit-pipeline svndiff1 absent-entries depth )'
117 ' 25:svn+ssh://example.com/svn 38:SVN/1.8.11'
118 ' (x86_64-apple-darwin14.1.0) ( {expected_client}) ) '.format(
119 expected_client=expected_client))
120
121 def test_kwargs_override_data_in_patch_first_client_response(self):
122 response = {
123 'version': 2,
124 'capabilities': 'edit-pipeline svndiff1 absent-entries depth',
125 'url': 'svn+ssh://example.com/svn',
126 'ra_client': 'SVN/1.8.11 (x86_64-apple-darwin14.1.0)',
127 'client': 'test'
128 }
129 wrapper = SubversionTunnelWrapper(timeout=5)
130 stdin = BytesIO()
131 with patch.object(wrapper, 'process') as process_mock:
132 process_mock.stdin = stdin
133 wrapper.patch_first_client_response(
134 response, version=3, client='abcde ',
135 capabilities='absent-entries depth',
136 url='svn+ssh://example.org/test',
137 ra_client='SVN/1.8.12 (ubuntu 14.04)')
138 assert (
139 stdin.getvalue() ==
140 '( 3 ( absent-entries depth ) 26:svn+ssh://example.org/test'
141 ' 25:SVN/1.8.12 (ubuntu 14.04) ( abcde ) ) ')
142
143 def test_patch_first_client_response_sets_environment(self):
144 response = {
145 'version': 2,
146 'capabilities': 'edit-pipeline svndiff1 absent-entries depth',
147 'url': 'svn+ssh://example.com/svn',
148 'ra_client': 'SVN/1.8.11 (x86_64-apple-darwin14.1.0)',
149 'client': 'test'
150 }
151 wrapper = SubversionTunnelWrapper(timeout=5)
152 stdin = BytesIO()
153 with patch.object(wrapper, 'create_hooks_env') as create_hooks_mock:
154 with patch.object(wrapper, 'process') as process_mock:
155 process_mock.stdin = stdin
156 wrapper.patch_first_client_response(response)
157 create_hooks_mock.assert_called_once_with()
158
159 def test_get_first_client_response_exits_by_signal(self):
160 wrapper = SubversionTunnelWrapper(timeout=1)
161 read_patch = patch.object(wrapper, '_read_first_client_response')
162 parse_patch = patch.object(wrapper, '_parse_first_client_response')
163 interrupt_patch = patch.object(wrapper, 'interrupt')
164
165 with read_patch as read_mock, parse_patch as parse_mock, \
166 interrupt_patch as interrupt_mock:
167 read_mock.side_effect = lambda: sleep(3)
168 wrapper.get_first_client_response()
169
170 assert parse_mock.call_count == 0
171 assert interrupt_mock.call_count == 1
172
173 def test_get_first_client_response_parses_data(self):
174 wrapper = SubversionTunnelWrapper(timeout=5)
175 response = (
176 '( 2 ( edit-pipeline svndiff1 absent-entries depth mergeinfo'
177 ' log-revprops ) 26:svn+ssh://vcs@vm/hello-svn 38:SVN/1.8.11'
178 ' (x86_64-apple-darwin14.1.0) ( ) ) ')
179 read_patch = patch.object(wrapper, '_read_first_client_response')
180 parse_patch = patch.object(wrapper, '_parse_first_client_response')
181
182 with read_patch as read_mock, parse_patch as parse_mock:
183 read_mock.return_value = response
184 wrapper.get_first_client_response()
185
186 parse_mock.assert_called_once_with(response)
187
188 def test_return_code(self):
189 wrapper = SubversionTunnelWrapper(timeout=5)
190 with patch.object(wrapper, 'process') as process_mock:
191 process_mock.returncode = 1
192 assert wrapper.return_code == 1
193
194 def test_sync_loop_breaks_when_process_cannot_be_polled(self):
195 self.counter = 0
196 buffer_ = 'abcdefghij'
197
198 wrapper = SubversionTunnelWrapper(timeout=5)
199 wrapper.stdin = BytesIO(buffer_)
200 with patch.object(wrapper, 'remove_configs') as remove_configs_mock:
201 with patch.object(wrapper, 'process') as process_mock:
202 process_mock.poll.side_effect = self._poll
203 process_mock.stdin = BytesIO()
204 wrapper.sync()
205 assert process_mock.stdin.getvalue() == 'abcde'
206 remove_configs_mock.assert_called_once_with()
207
208 def test_sync_loop_breaks_when_nothing_to_read(self):
209 self.counter = 0
210 buffer_ = 'abcdefghij'
211
212 wrapper = SubversionTunnelWrapper(timeout=5)
213 wrapper.stdin = BytesIO(buffer_)
214 with patch.object(wrapper, 'remove_configs') as remove_configs_mock:
215 with patch.object(wrapper, 'process') as process_mock:
216 process_mock.poll.return_value = None
217 process_mock.stdin = BytesIO()
218 wrapper.sync()
219 assert process_mock.stdin.getvalue() == buffer_
220 remove_configs_mock.assert_called_once_with()
221
222 def test_start_without_repositories_root(self):
223 svn_path = '/usr/local/bin/svnserve'
224 wrapper = SubversionTunnelWrapper(timeout=5, svn_path=svn_path)
225 with patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.Popen') as popen_mock:
226 wrapper.start()
227 expected_command = [
228 svn_path, '-t', '--config-file', wrapper.svn_conf_path]
229 popen_mock.assert_called_once_with(
230 expected_command, stdin=subprocess.PIPE)
231 assert wrapper.process == popen_mock()
232
233 def test_start_with_repositories_root(self):
234 svn_path = '/usr/local/bin/svnserve'
235 repositories_root = '/home/repos'
236 wrapper = SubversionTunnelWrapper(
237 timeout=5, svn_path=svn_path, repositories_root=repositories_root)
238 with patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.Popen') as popen_mock:
239 wrapper.start()
240 expected_command = [
241 svn_path, '-t', '--config-file', wrapper.svn_conf_path,
242 '-r', repositories_root]
243 popen_mock.assert_called_once_with(
244 expected_command, stdin=subprocess.PIPE)
245 assert wrapper.process == popen_mock()
246
247 def test_create_svn_config(self):
248 wrapper = SubversionTunnelWrapper(timeout=5)
249 file_mock = MagicMock(spec=file)
250 with patch('os.fdopen', create=True) as open_mock:
251 open_mock.return_value = file_mock
252 wrapper.create_svn_config()
253 open_mock.assert_called_once_with(wrapper.svn_conf_fd, 'w')
254 expected_content = '[general]\nhooks-env = {}\n'.format(
255 wrapper.hooks_env_path)
256 file_handle = file_mock.__enter__.return_value
257 file_handle.write.assert_called_once_with(expected_content)
258
259 @pytest.mark.parametrize(
260 'read_only, expected_content', [
261 [True, '[default]\nLANG = en_US.UTF-8\nSSH_READ_ONLY = 1\n'],
262 [False, '[default]\nLANG = en_US.UTF-8\n']
263 ], ids=no_newline_id_generator)
264 def test_create_hooks_env(self, read_only, expected_content):
265 wrapper = SubversionTunnelWrapper(timeout=5)
266 wrapper.read_only = read_only
267 file_mock = MagicMock(spec=file)
268 with patch('os.fdopen', create=True) as open_mock:
269 open_mock.return_value = file_mock
270 wrapper.create_hooks_env()
271 open_mock.assert_called_once_with(wrapper.hooks_env_fd, 'w')
272 file_handle = file_mock.__enter__.return_value
273 file_handle.write.assert_called_once_with(expected_content)
274
275 def test_remove_configs(self):
276 wrapper = SubversionTunnelWrapper(timeout=5)
277 with patch('os.remove') as remove_mock:
278 wrapper.remove_configs()
279 expected_calls = [
280 call(wrapper.svn_conf_path), call(wrapper.hooks_env_path)]
281 assert sorted(remove_mock.call_args_list) == sorted(expected_calls)
282
283 def _poll(self):
284 self.counter += 1
285 return None if self.counter < 6 else 1
@@ -603,14 +603,36 b' ssh.generate_authorized_keyfile = false'
603 # ssh.authorized_keys_ssh_opts =
603 # ssh.authorized_keys_ssh_opts =
604
604
605 ## File to generate the authorized keys together with options
605 ## File to generate the authorized keys together with options
606 ssh.authorized_keys_file_path = /home/USER/.ssh/authorized_keys
606 ## It is possible to have multiple key files specified in `sshd_config` e.g.
607 ## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
608 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
609
610 ## Command to execute the SSH wrapper. The binary is available in the
611 ## rhodecode installation directory.
612 ## e.g ~/.rccontrol/community-1/profile/bin/rcssh-wrapper
613 ssh.wrapper_cmd = ~/.rccontrol/community-1/rcssh-wrapper
614
615 ## Allow shell when executing the ssh-wrapper command
616 ssh.wrapper_cmd_allow_shell = false
607
617
608 ## Command to execute as an SSH wrapper, available from
618 ## Enables logging, and detailed output send back to the client. Usefull for
609 ## https://code.rhodecode.com/rhodecode-ssh
619 ## debugging, shouldn't be used in production.
610 ssh.wrapper_cmd = /home/USER/rhodecode-ssh/sshwrapper.py
620 ssh.enable_debug_logging = false
621
622 ## API KEY for user who has access to fetch other user permission information
623 ## most likely an super-admin account with some IP restrictions.
624 ssh.api_key =
611
625
612 ## Allow shell when executing the command
626 ## API Host, the server address of RhodeCode instance that the api_key will
613 ssh.wrapper_cmd_allow_shell = false
627 ## access
628 ssh.api_host = http://localhost
629
630 ## Paths to binary executrables, by default they are the names, but we can
631 ## override them if we want to use a custom one
632 ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg
633 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
634 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
635
614
636
615 ## Dummy marker to add new entries after.
637 ## Dummy marker to add new entries after.
616 ## Add any custom entries below. Please don't remove.
638 ## Add any custom entries below. Please don't remove.
@@ -621,7 +643,7 b' custom.conf = 1'
621 ### LOGGING CONFIGURATION ####
643 ### LOGGING CONFIGURATION ####
622 ################################
644 ################################
623 [loggers]
645 [loggers]
624 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
646 keys = root, routes, rhodecode, sqlalchemy, beaker, templates, ssh_wrapper
625
647
626 [handlers]
648 [handlers]
627 keys = console, console_sql
649 keys = console, console_sql
@@ -667,6 +689,13 b' handlers = console_sql'
667 qualname = sqlalchemy.engine
689 qualname = sqlalchemy.engine
668 propagate = 0
690 propagate = 0
669
691
692 [logger_ssh_wrapper]
693 level = DEBUG
694 handlers =
695 qualname = ssh_wrapper
696 propagate = 1
697
698
670 ##############
699 ##############
671 ## HANDLERS ##
700 ## HANDLERS ##
672 ##############
701 ##############
@@ -572,14 +572,36 b' ssh.generate_authorized_keyfile = false'
572 # ssh.authorized_keys_ssh_opts =
572 # ssh.authorized_keys_ssh_opts =
573
573
574 ## File to generate the authorized keys together with options
574 ## File to generate the authorized keys together with options
575 ssh.authorized_keys_file_path = /home/USER/.ssh/authorized_keys
575 ## It is possible to have multiple key files specified in `sshd_config` e.g.
576 ## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
577 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
578
579 ## Command to execute the SSH wrapper. The binary is available in the
580 ## rhodecode installation directory.
581 ## e.g ~/.rccontrol/community-1/profile/bin/rcssh-wrapper
582 ssh.wrapper_cmd = ~/.rccontrol/community-1/rcssh-wrapper
583
584 ## Allow shell when executing the ssh-wrapper command
585 ssh.wrapper_cmd_allow_shell = false
576
586
577 ## Command to execute as an SSH wrapper, available from
587 ## Enables logging, and detailed output send back to the client. Usefull for
578 ## https://code.rhodecode.com/rhodecode-ssh
588 ## debugging, shouldn't be used in production.
579 ssh.wrapper_cmd = /home/USER/rhodecode-ssh/sshwrapper.py
589 ssh.enable_debug_logging = false
590
591 ## API KEY for user who has access to fetch other user permission information
592 ## most likely an super-admin account with some IP restrictions.
593 ssh.api_key =
580
594
581 ## Allow shell when executing the command
595 ## API Host, the server address of RhodeCode instance that the api_key will
582 ssh.wrapper_cmd_allow_shell = false
596 ## access
597 ssh.api_host = http://localhost
598
599 ## Paths to binary executrables, by default they are the names, but we can
600 ## override them if we want to use a custom one
601 ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg
602 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
603 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
604
583
605
584 ## Dummy marker to add new entries after.
606 ## Dummy marker to add new entries after.
585 ## Add any custom entries below. Please don't remove.
607 ## Add any custom entries below. Please don't remove.
@@ -590,7 +612,7 b' custom.conf = 1'
590 ### LOGGING CONFIGURATION ####
612 ### LOGGING CONFIGURATION ####
591 ################################
613 ################################
592 [loggers]
614 [loggers]
593 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
615 keys = root, routes, rhodecode, sqlalchemy, beaker, templates, ssh_wrapper
594
616
595 [handlers]
617 [handlers]
596 keys = console, console_sql
618 keys = console, console_sql
@@ -636,6 +658,13 b' handlers = console_sql'
636 qualname = sqlalchemy.engine
658 qualname = sqlalchemy.engine
637 propagate = 0
659 propagate = 0
638
660
661 [logger_ssh_wrapper]
662 level = DEBUG
663 handlers =
664 qualname = ssh_wrapper
665 propagate = 1
666
667
639 ##############
668 ##############
640 ## HANDLERS ##
669 ## HANDLERS ##
641 ##############
670 ##############
@@ -33,6 +33,7 b' from rhodecode.lib import user_sessions'
33 from rhodecode.lib.utils2 import safe_int
33 from rhodecode.lib.utils2 import safe_int
34 from rhodecode.model.db import UserIpMap
34 from rhodecode.model.db import UserIpMap
35 from rhodecode.model.scm import ScmModel
35 from rhodecode.model.scm import ScmModel
36 from rhodecode.model.settings import VcsSettingsModel
36
37
37 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
38
39
@@ -75,6 +76,35 b' def get_server_info(request, apiuser):'
75
76
76
77
77 @jsonrpc_method()
78 @jsonrpc_method()
79 def get_repo_store(request, apiuser):
80 """
81 Returns the |RCE| repository storage information.
82
83 :param apiuser: This is filled automatically from the |authtoken|.
84 :type apiuser: AuthUser
85
86 Example output:
87
88 .. code-block:: bash
89
90 id : <id_given_in_input>
91 result : {
92 'modules': [<module name>,...]
93 'py_version': <python version>,
94 'platform': <platform type>,
95 'rhodecode_version': <rhodecode version>
96 }
97 error : null
98 """
99
100 if not has_superadmin_permission(apiuser):
101 raise JSONRPCForbidden()
102
103 path = VcsSettingsModel().get_repos_location()
104 return {"path": path}
105
106
107 @jsonrpc_method()
78 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
108 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
79 """
109 """
80 Displays the IP Address as seen from the |RCE| server.
110 Displays the IP Address as seen from the |RCE| server.
@@ -35,14 +35,30 b' def _sanitize_settings_and_apply_default'
35 """
35 """
36 _bool_setting(settings, config_keys.generate_authorized_keyfile, 'false')
36 _bool_setting(settings, config_keys.generate_authorized_keyfile, 'false')
37 _bool_setting(settings, config_keys.wrapper_allow_shell, 'false')
37 _bool_setting(settings, config_keys.wrapper_allow_shell, 'false')
38 _bool_setting(settings, config_keys.enable_debug_logging, 'false')
38
39
39 _string_setting(settings, config_keys.authorized_keys_file_path, '',
40 _string_setting(settings, config_keys.authorized_keys_file_path,
41 '~/.ssh/authorized_keys_rhodecode',
40 lower=False)
42 lower=False)
41 _string_setting(settings, config_keys.wrapper_cmd, '',
43 _string_setting(settings, config_keys.wrapper_cmd, '',
42 lower=False)
44 lower=False)
43 _string_setting(settings, config_keys.authorized_keys_line_ssh_opts, '',
45 _string_setting(settings, config_keys.authorized_keys_line_ssh_opts, '',
44 lower=False)
46 lower=False)
45
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 _string_setting(settings, config_keys.ssh_hg_bin,
53 '~/.rccontrol/vcsserver-1/profile/bin/hg',
54 lower=False)
55 _string_setting(settings, config_keys.ssh_git_bin,
56 '~/.rccontrol/vcsserver-1/profile/bin/git',
57 lower=False)
58 _string_setting(settings, config_keys.ssh_svn_bin,
59 '~/.rccontrol/vcsserver-1/profile/bin/svnserve',
60 lower=False)
61
46
62
47 def includeme(config):
63 def includeme(config):
48 settings = config.registry.settings
64 settings = config.registry.settings
@@ -26,3 +26,11 b" authorized_keys_file_path = 'ssh.authori"
26 authorized_keys_line_ssh_opts = 'ssh.authorized_keys_ssh_opts'
26 authorized_keys_line_ssh_opts = 'ssh.authorized_keys_ssh_opts'
27 wrapper_cmd = 'ssh.wrapper_cmd'
27 wrapper_cmd = 'ssh.wrapper_cmd'
28 wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell'
28 wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell'
29 enable_debug_logging = 'ssh.enable_debug_logging'
30
31 ssh_api_key = 'ssh.api_key'
32 ssh_api_host = 'ssh.api_host'
33
34 ssh_hg_bin = 'ssh.executable.hg'
35 ssh_git_bin = 'ssh.executable.git'
36 ssh_svn_bin = 'ssh.executable.svn'
@@ -31,8 +31,9 b' from rhodecode.lib.utils2 import Attribu'
31 class TestSshKeyFileGeneration(object):
31 class TestSshKeyFileGeneration(object):
32 @pytest.mark.parametrize('ssh_wrapper_cmd', ['/tmp/sshwrapper.py'])
32 @pytest.mark.parametrize('ssh_wrapper_cmd', ['/tmp/sshwrapper.py'])
33 @pytest.mark.parametrize('allow_shell', [True, False])
33 @pytest.mark.parametrize('allow_shell', [True, False])
34 @pytest.mark.parametrize('debug', [True, False])
34 @pytest.mark.parametrize('ssh_opts', [None, 'mycustom,option'])
35 @pytest.mark.parametrize('ssh_opts', [None, 'mycustom,option'])
35 def test_write_keyfile(self, tmpdir, ssh_wrapper_cmd, allow_shell, ssh_opts):
36 def test_write_keyfile(self, tmpdir, ssh_wrapper_cmd, allow_shell, debug, ssh_opts):
36
37
37 authorized_keys_file_path = os.path.join(str(tmpdir), 'authorized_keys')
38 authorized_keys_file_path = os.path.join(str(tmpdir), 'authorized_keys')
38
39
@@ -43,26 +44,30 b' class TestSshKeyFileGeneration(object):'
43 AttributeDict({'user': AttributeDict(username='user'),
44 AttributeDict({'user': AttributeDict(username='user'),
44 'ssh_key_data': 'ssh-rsa USER_KEY'}),
45 'ssh_key_data': 'ssh-rsa USER_KEY'}),
45 ]
46 ]
46
47 with mock.patch('rhodecode.apps.ssh_support.utils.get_all_active_keys',
47 with mock.patch('rhodecode.apps.ssh_support.utils.get_all_active_keys',
48 return_value=keys()):
48 return_value=keys()):
49 utils._generate_ssh_authorized_keys_file(
49 with mock.patch.dict('rhodecode.CONFIG', {'__file__': '/tmp/file.ini'}):
50 authorized_keys_file_path, ssh_wrapper_cmd,
50 utils._generate_ssh_authorized_keys_file(
51 allow_shell, ssh_opts
51 authorized_keys_file_path, ssh_wrapper_cmd,
52 )
52 allow_shell, ssh_opts, debug
53 )
53
54
54 assert os.path.isfile(authorized_keys_file_path)
55 assert os.path.isfile(authorized_keys_file_path)
55 with open(authorized_keys_file_path) as f:
56 with open(authorized_keys_file_path) as f:
56 content = f.read()
57 content = f.read()
57
58
58 assert 'command="/tmp/sshwrapper.py' in content
59 assert 'command="/tmp/sshwrapper.py' in content
59 assert 'This file is managed by RhodeCode, ' \
60 assert 'This file is managed by RhodeCode, ' \
60 'please do not edit it manually.' in content
61 'please do not edit it manually.' in content
62
63 if allow_shell:
64 assert '--shell' in content
61
65
62 if allow_shell:
66 if debug:
63 assert '--shell --user' in content
67 assert '--debug' in content
64 else:
68
65 assert '--user' in content
69 assert '--user' in content
70 assert '--user-id' in content
66
71
67 if ssh_opts:
72 if ssh_opts:
68 assert ssh_opts in content
73 assert ssh_opts in content
@@ -48,11 +48,15 b' def get_all_active_keys():'
48
48
49
49
50 def _generate_ssh_authorized_keys_file(
50 def _generate_ssh_authorized_keys_file(
51 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts):
51 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts, debug):
52
53 import rhodecode
52 all_active_keys = get_all_active_keys()
54 all_active_keys = get_all_active_keys()
53
55
54 if allow_shell:
56 if allow_shell:
55 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
57 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
58 if debug:
59 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --debug'
56
60
57 if not os.path.isfile(authorized_keys_file_path):
61 if not os.path.isfile(authorized_keys_file_path):
58 with open(authorized_keys_file_path, 'w'):
62 with open(authorized_keys_file_path, 'w'):
@@ -62,7 +66,7 b' def _generate_ssh_authorized_keys_file('
62 raise OSError('Access to file {} is without read access'.format(
66 raise OSError('Access to file {} is without read access'.format(
63 authorized_keys_file_path))
67 authorized_keys_file_path))
64
68
65 line_tmpl = '{ssh_opts},command="{wrapper_command} --user {user}" {key}\n'
69 line_tmpl = '{ssh_opts},command="{wrapper_command} {ini_path} --user-id={user_id} --user={user}" {key}\n'
66
70
67 fd, tmp_authorized_keys = tempfile.mkstemp(
71 fd, tmp_authorized_keys = tempfile.mkstemp(
68 '.authorized_keys_write',
72 '.authorized_keys_write',
@@ -71,13 +75,18 b' def _generate_ssh_authorized_keys_file('
71 now = datetime.datetime.utcnow().isoformat()
75 now = datetime.datetime.utcnow().isoformat()
72 keys_file = os.fdopen(fd, 'wb')
76 keys_file = os.fdopen(fd, 'wb')
73 keys_file.write(HEADER.format(len(all_active_keys), now))
77 keys_file.write(HEADER.format(len(all_active_keys), now))
78 ini_path = rhodecode.CONFIG['__file__']
74
79
75 for user_key in all_active_keys:
80 for user_key in all_active_keys:
76 username = user_key.user.username
81 username = user_key.user.username
82 user_id = user_key.user.user_id
83
77 keys_file.write(
84 keys_file.write(
78 line_tmpl.format(
85 line_tmpl.format(
79 ssh_opts=ssh_opts or SSH_OPTS,
86 ssh_opts=ssh_opts or SSH_OPTS,
80 wrapper_command=ssh_wrapper_cmd,
87 wrapper_command=ssh_wrapper_cmd,
88 ini_path=ini_path,
89 user_id=user_id,
81 user=username, key=user_key.ssh_key_data))
90 user=username, key=user_key.ssh_key_data))
82 log.debug('addkey: Key added for user: `%s`', username)
91 log.debug('addkey: Key added for user: `%s`', username)
83 keys_file.close()
92 keys_file.close()
@@ -100,8 +109,11 b' def generate_ssh_authorized_keys_file(re'
100 config_keys.wrapper_allow_shell)
109 config_keys.wrapper_allow_shell)
101 ssh_opts = registry.settings.get(
110 ssh_opts = registry.settings.get(
102 config_keys.authorized_keys_line_ssh_opts)
111 config_keys.authorized_keys_line_ssh_opts)
112 debug = registry.settings.get(
113 config_keys.enable_debug_logging)
103
114
104 _generate_ssh_authorized_keys_file(
115 _generate_ssh_authorized_keys_file(
105 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts)
116 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts,
117 debug)
106
118
107 return 0
119 return 0
@@ -249,14 +249,18 b' gist_alias_url ='
249 ## used for access.
249 ## used for access.
250 ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
250 ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
251 ## came from the the logged in user who own this authentication token.
251 ## came from the the logged in user who own this authentication token.
252 ## Additionally @TOKEN syntaxt can be used to bound the view to specific
253 ## authentication token. Such view would be only accessible when used together
254 ## with this authentication token
252 ##
255 ##
253 ## list of all views can be found under `_admin/permissions/auth_token_access`
256 ## list of all views can be found under `/_admin/permissions/auth_token_access`
254 ## The list should be "," separated and on a single line.
257 ## The list should be "," separated and on a single line.
255 ##
258 ##
256 ## Most common views to enable:
259 ## Most common views to enable:
257 # RepoCommitsView:repo_commit_download
260 # RepoCommitsView:repo_commit_download
258 # RepoCommitsView:repo_commit_patch
261 # RepoCommitsView:repo_commit_patch
259 # RepoCommitsView:repo_commit_raw
262 # RepoCommitsView:repo_commit_raw
263 # RepoCommitsView:repo_commit_raw@TOKEN
260 # RepoFilesView:repo_files_diff
264 # RepoFilesView:repo_files_diff
261 # RepoFilesView:repo_archivefile
265 # RepoFilesView:repo_archivefile
262 # RepoFilesView:repo_file_raw
266 # RepoFilesView:repo_file_raw
@@ -587,7 +591,7 b' vcs.server = localhost:9901'
587 vcs.server.protocol = http
591 vcs.server.protocol = http
588
592
589 ## Push/Pull operations protocol, available options are:
593 ## Push/Pull operations protocol, available options are:
590 ## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended
594 ## `http` - use http-rpc backend (default)
591 ## `vcsserver.scm_app` - internal app (EE only)
595 ## `vcsserver.scm_app` - internal app (EE only)
592 vcs.scm_app_implementation = http
596 vcs.scm_app_implementation = http
593
597
@@ -607,7 +611,7 b' vcs.backends = hg, git, svn'
607
611
608 vcs.connection_timeout = 3600
612 vcs.connection_timeout = 3600
609 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
613 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
610 ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible
614 ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
611 #vcs.svn.compatible_version = pre-1.8-compatible
615 #vcs.svn.compatible_version = pre-1.8-compatible
612
616
613
617
@@ -631,6 +635,49 b' svn.proxy.location_root = /'
631 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
635 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
632 #svn.proxy.reload_timeout = 10
636 #svn.proxy.reload_timeout = 10
633
637
638 ############################################################
639 ### SSH Support Settings ###
640 ############################################################
641
642 ## Defines if the authorized_keys file should be written on any change of
643 ## user ssh keys
644 ssh.generate_authorized_keyfile = false
645
646 ## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
647 # ssh.authorized_keys_ssh_opts =
648
649 ## File to generate the authorized keys together with options
650 ## It is possible to have multiple key files specified in `sshd_config` e.g.
651 ## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
652 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
653
654 ## Command to execute the SSH wrapper. The binary is available in the
655 ## rhodecode installation directory.
656 ## e.g ~/.rccontrol/community-1/profile/bin/rcssh-wrapper
657 ssh.wrapper_cmd = ~/.rccontrol/community-1/rcssh-wrapper
658
659 ## Allow shell when executing the ssh-wrapper command
660 ssh.wrapper_cmd_allow_shell = false
661
662 ## Enables logging, and detailed output send back to the client. Usefull for
663 ## debugging, shouldn't be used in production.
664 ssh.enable_debug_logging = false
665
666 ## API KEY for user who has access to fetch other user permission information
667 ## most likely an super-admin account with some IP restrictions.
668 ssh.api_key =
669
670 ## API Host, the server address of RhodeCode instance that the api_key will
671 ## access
672 ssh.api_host = http://localhost
673
674 ## Paths to binary executrables, by default they are the names, but we can
675 ## override them if we want to use a custom one
676 ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg
677 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
678 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
679
680
634 ## Dummy marker to add new entries after.
681 ## Dummy marker to add new entries after.
635 ## Add any custom entries below. Please don't remove.
682 ## Add any custom entries below. Please don't remove.
636 custom.conf = 1
683 custom.conf = 1
@@ -640,7 +687,7 b' custom.conf = 1'
640 ### LOGGING CONFIGURATION ####
687 ### LOGGING CONFIGURATION ####
641 ################################
688 ################################
642 [loggers]
689 [loggers]
643 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
690 keys = root, routes, rhodecode, sqlalchemy, beaker, templates, ssh_wrapper
644
691
645 [handlers]
692 [handlers]
646 keys = console, console_sql
693 keys = console, console_sql
@@ -686,6 +733,13 b' handlers = console_sql'
686 qualname = sqlalchemy.engine
733 qualname = sqlalchemy.engine
687 propagate = 0
734 propagate = 0
688
735
736 [logger_ssh_wrapper]
737 level = DEBUG
738 handlers =
739 qualname = ssh_wrapper
740 propagate = 1
741
742
689 ##############
743 ##############
690 ## HANDLERS ##
744 ## HANDLERS ##
691 ##############
745 ##############
@@ -251,6 +251,7 b' setup('
251 'rcserver=rhodecode.rcserver:main',
251 'rcserver=rhodecode.rcserver:main',
252 'rcsetup-app=rhodecode.lib.rc_commands.setup_rc:main',
252 'rcsetup-app=rhodecode.lib.rc_commands.setup_rc:main',
253 'rcupgrade-db=rhodecode.lib.rc_commands.upgrade_db:main',
253 'rcupgrade-db=rhodecode.lib.rc_commands.upgrade_db:main',
254 'rcssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper:main',
254 ],
255 ],
255 'beaker.backends': [
256 'beaker.backends': [
256 'memorylru_base=rhodecode.lib.memory_lru_debug:MemoryLRUNamespaceManagerBase',
257 'memorylru_base=rhodecode.lib.memory_lru_debug:MemoryLRUNamespaceManagerBase',
General Comments 0
You need to be logged in to leave comments. Login now