##// END OF EJS Templates
ssh-support: enabled full handling of all backends via SSH....
marcink -
r2187:47a0c0ed default
parent child Browse files
Show More
@@ -0,0 +1,202 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 logging
24 import datetime
25 import ConfigParser
26
27 from rhodecode.model.db import Session, User, UserSshKeys
28 from rhodecode.model.scm import ScmModel
29
30 from .hg import MercurialServer
31 from .git import GitServer
32 from .svn import SubversionServer
33 log = logging.getLogger(__name__)
34
35
36 class SshWrapper(object):
37
38 def __init__(self, command, connection_info, mode,
39 user, user_id, key_id, shell, ini_path, env):
40 self.command = command
41 self.connection_info = connection_info
42 self.mode = mode
43 self.user = user
44 self.user_id = user_id
45 self.key_id = key_id
46 self.shell = shell
47 self.ini_path = ini_path
48 self.env = env
49
50 self.config = self.parse_config(ini_path)
51 self.server_impl = None
52
53 def parse_config(self, config_path):
54 parser = ConfigParser.ConfigParser()
55 parser.read(config_path)
56 return parser
57
58 def update_key_access_time(self, key_id):
59 key = UserSshKeys().query().filter(
60 UserSshKeys.ssh_key_id == key_id).scalar()
61 if key:
62 key.accessed_on = datetime.datetime.utcnow()
63 Session().add(key)
64 Session().commit()
65 log.debug('Update key `%s` access time', key_id)
66
67 def get_connection_info(self):
68 """
69 connection_info
70
71 Identifies the client and server ends of the connection.
72 The variable contains four space-separated values: client IP address,
73 client port number, server IP address, and server port number.
74 """
75 conn = dict(
76 client_ip=None,
77 client_port=None,
78 server_ip=None,
79 server_port=None,
80 )
81
82 info = self.connection_info.split(' ')
83 if len(info) == 4:
84 conn['client_ip'] = info[0]
85 conn['client_port'] = info[1]
86 conn['server_ip'] = info[2]
87 conn['server_port'] = info[3]
88
89 return conn
90
91 def get_repo_details(self, mode):
92 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
93 mode = mode
94 repo_name = None
95
96 hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$'
97 hg_match = re.match(hg_pattern, self.command)
98 if hg_match is not None:
99 vcs_type = 'hg'
100 repo_name = hg_match.group(1).strip('/')
101 return vcs_type, repo_name, mode
102
103 git_pattern = (
104 r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
105 git_match = re.match(git_pattern, self.command)
106 if git_match is not None:
107 vcs_type = 'git'
108 repo_name = git_match.group(2).strip('/')
109 mode = git_match.group(1)
110 return vcs_type, repo_name, mode
111
112 svn_pattern = r'^svnserve -t'
113 svn_match = re.match(svn_pattern, self.command)
114
115 if svn_match is not None:
116 vcs_type = 'svn'
117 # Repo name should be extracted from the input stream
118 return vcs_type, repo_name, mode
119
120 return vcs_type, repo_name, mode
121
122 def serve(self, vcs, repo, mode, user, permissions):
123 store = ScmModel().repos_path
124
125 log.debug(
126 'VCS detected:`%s` mode: `%s` repo_name: %s', vcs, mode, repo)
127
128 if vcs == 'hg':
129 server = MercurialServer(
130 store=store, ini_path=self.ini_path,
131 repo_name=repo, user=user,
132 user_permissions=permissions, config=self.config, env=self.env)
133 self.server_impl = server
134 return server.run()
135
136 elif vcs == 'git':
137 server = GitServer(
138 store=store, ini_path=self.ini_path,
139 repo_name=repo, repo_mode=mode, user=user,
140 user_permissions=permissions, config=self.config, env=self.env)
141 self.server_impl = server
142 return server.run()
143
144 elif vcs == 'svn':
145 server = SubversionServer(
146 store=store, ini_path=self.ini_path,
147 repo_name=None, user=user,
148 user_permissions=permissions, config=self.config, env=self.env)
149 self.server_impl = server
150 return server.run()
151
152 else:
153 raise Exception('Unrecognised VCS: {}'.format(vcs))
154
155 def wrap(self):
156 mode = self.mode
157 user = self.user
158 user_id = self.user_id
159 key_id = self.key_id
160 shell = self.shell
161
162 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
163
164 log.debug(
165 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` '
166 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
167 mode, user, user_id, shell, self.command,
168 scm_detected, scm_mode, scm_repo)
169
170 # update last access time for this key
171 self.update_key_access_time(key_id)
172
173 log.debug('SSH Connection info %s', self.get_connection_info())
174
175 if shell and self.command is None:
176 log.info(
177 'Dropping to shell, no command given and shell is allowed')
178 os.execl('/bin/bash', '-l')
179 exit_code = 1
180
181 elif scm_detected:
182 user = User.get(user_id)
183 auth_user = user.AuthUser()
184 permissions = auth_user.permissions['repositories']
185
186 try:
187 exit_code, is_updated = self.serve(
188 scm_detected, scm_repo, scm_mode, user, permissions)
189 except Exception:
190 log.exception('Error occurred during execution of SshWrapper')
191 exit_code = -1
192
193 elif self.command is None and shell is False:
194 log.error('No Command given.')
195 exit_code = -1
196
197 else:
198 log.error(
199 'Unhandled Command: "%s" Aborting.', self.command)
200 exit_code = -1
201
202 return exit_code
@@ -0,0 +1,149 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 sys
23 import json
24 import logging
25
26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
27 from rhodecode.lib import hooks_utils
28 from rhodecode.model.scm import ScmModel
29
30 log = logging.getLogger(__name__)
31
32
33 class VcsServer(object):
34 _path = None # set executable path for hg/git/svn binary
35 backend = None # set in child classes
36 tunnel = None # subprocess handling tunnel
37 write_perms = ['repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39
40 def __init__(self, user, user_permissions, config, env):
41 self.user = user
42 self.user_permissions = user_permissions
43 self.config = config
44 self.env = env
45 self.stdin = sys.stdin
46
47 self.repo_name = None
48 self.repo_mode = None
49 self.store = ''
50 self.ini_path = ''
51
52 def _invalidate_cache(self, repo_name):
53 """
54 Set's cache for this repository for invalidation on next access
55
56 :param repo_name: full repo name, also a cache key
57 """
58 ScmModel().mark_for_invalidation(repo_name)
59
60 def has_write_perm(self):
61 permission = self.user_permissions.get(self.repo_name)
62 if permission in ['repository.write', 'repository.admin']:
63 return True
64
65 return False
66
67 def _check_permissions(self, action):
68 permission = self.user_permissions.get(self.repo_name)
69 log.debug(
70 'permission for %s on %s are: %s',
71 self.user, self.repo_name, permission)
72
73 if action == 'pull':
74 if permission in self.read_perms:
75 log.info(
76 'READ Permissions for User "%s" detected to repo "%s"!',
77 self.user, self.repo_name)
78 return 0
79 else:
80 if permission in self.write_perms:
81 log.info(
82 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
83 self.user, self.repo_name)
84 return 0
85
86 log.error('Cannot properly fetch or allow user permissions. '
87 'Return value is: %s, req action: %s', permission, action)
88 return -2
89
90 def update_environment(self, action, extras=None):
91
92 scm_data = {
93 'ip': os.environ['SSH_CLIENT'].split()[0],
94 'username': self.user.username,
95 'action': action,
96 'repository': self.repo_name,
97 'scm': self.backend,
98 'config': self.ini_path,
99 'make_lock': None,
100 'locked_by': [None, None],
101 'server_url': None,
102 'is_shadow_repo': False,
103 'hooks_module': 'rhodecode.lib.hooks_daemon',
104 'hooks': ['push', 'pull'],
105 'SSH': True,
106 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name)
107 }
108 if extras:
109 scm_data.update(extras)
110 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
111
112 def get_root_store(self):
113 root_store = self.store
114 if not root_store.endswith('/'):
115 # always append trailing slash
116 root_store = root_store + '/'
117 return root_store
118
119 def _handle_tunnel(self, extras):
120 # pre-auth
121 action = 'pull'
122 exit_code = self._check_permissions(action)
123 if exit_code:
124 return exit_code, False
125
126 req = self.env['request']
127 server_url = req.host_url + req.script_name
128 extras['server_url'] = server_url
129
130 log.debug('Using %s binaries from path %s', self.backend, self._path)
131 exit_code = self.tunnel.run(extras)
132
133 return exit_code, action == "push"
134
135 def run(self):
136 extras = {}
137 HOOKS_PROTOCOL = self.config.get('app:main', 'vcs.hooks.protocol')
138
139 callback_daemon, extras = prepare_callback_daemon(
140 extras, protocol=HOOKS_PROTOCOL,
141 use_direct_calls=False)
142
143 with callback_daemon:
144 try:
145 return self._handle_tunnel(extras)
146 finally:
147 log.debug('Running cleanup with cache invalidation')
148 if self.repo_name:
149 self._invalidate_cache(self.repo_name)
@@ -0,0 +1,75 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 sys
23 import logging
24
25 from .base import VcsServer
26
27 log = logging.getLogger(__name__)
28
29
30 class GitTunnelWrapper(object):
31 process = None
32
33 def __init__(self, server):
34 self.server = server
35 self.stdin = sys.stdin
36 self.stdout = sys.stdout
37
38 def create_hooks_env(self):
39 pass
40
41 def command(self):
42 root = self.server.get_root_store()
43 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
44 root=root, git_path=self.server.git_path,
45 mode=self.server.repo_mode, repo_name=self.server.repo_name)
46 log.debug("Final CMD: %s", command)
47 return command
48
49 def run(self, extras):
50 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
51 exit_code = self.server._check_permissions(action)
52 if exit_code:
53 return exit_code
54
55 self.server.update_environment(action=action, extras=extras)
56 self.create_hooks_env()
57 return os.system(self.command())
58
59
60 class GitServer(VcsServer):
61 backend = 'git'
62
63 def __init__(self, store, ini_path, repo_name, repo_mode,
64 user, user_permissions, config, env):
65 super(GitServer, self).\
66 __init__(user, user_permissions, config, env)
67
68 self.store = store
69 self.ini_path = ini_path
70 self.repo_name = repo_name
71 self._path = self.git_path = config.get(
72 'app:main', 'ssh.executable.git')
73
74 self.repo_mode = repo_mode
75 self.tunnel = GitTunnelWrapper(server=self)
@@ -0,0 +1,123 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 sys
23 import shutil
24 import logging
25 import tempfile
26 import textwrap
27
28 from .base import VcsServer
29
30 log = logging.getLogger(__name__)
31
32
33 class MercurialTunnelWrapper(object):
34 process = None
35
36 def __init__(self, server):
37 self.server = server
38 self.stdin = sys.stdin
39 self.stdout = sys.stdout
40 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
41 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
42
43 def create_hooks_env(self):
44
45 content = textwrap.dedent(
46 '''
47 # SSH hooks version=1.0.0
48 [hooks]
49 pretxnchangegroup.ssh_auth=python:vcsserver.hooks.pre_push_ssh_auth
50 pretxnchangegroup.ssh=python:vcsserver.hooks.pre_push_ssh
51 changegroup.ssh=python:vcsserver.hooks.post_push_ssh
52
53 preoutgoing.ssh=python:vcsserver.hooks.pre_pull_ssh
54 outgoing.ssh=python:vcsserver.hooks.post_pull_ssh
55
56 '''
57 )
58
59 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
60 hooks_env_file.write(content)
61 root = self.server.get_root_store()
62
63 hgrc_custom = os.path.join(
64 root, self.server.repo_name, '.hg', 'hgrc_rhodecode')
65 log.debug('Wrote custom hgrc file under %s', hgrc_custom)
66 shutil.move(
67 self.hooks_env_path, hgrc_custom)
68
69 hgrc_main = os.path.join(
70 root, self.server.repo_name, '.hg', 'hgrc')
71 include_marker = '%include hgrc_rhodecode'
72
73 if not os.path.isfile(hgrc_main):
74 os.mknod(hgrc_main)
75
76 with open(hgrc_main, 'rb') as f:
77 data = f.read()
78 has_marker = include_marker in data
79
80 if not has_marker:
81 log.debug('Adding include marker for hooks')
82 with open(hgrc_main, 'wa') as f:
83 f.write(textwrap.dedent('''
84 # added by RhodeCode
85 {}
86 '''.format(include_marker)))
87
88 def command(self):
89 root = self.server.get_root_store()
90
91 command = (
92 "cd {root}; {hg_path} -R {root}{repo_name} "
93 "serve --stdio".format(
94 root=root, hg_path=self.server.hg_path,
95 repo_name=self.server.repo_name))
96 log.debug("Final CMD: %s", command)
97 return command
98
99 def run(self, extras):
100 # at this point we cannot tell, we do further ACL checks
101 # inside the hooks
102 action = '?'
103 # permissions are check via `pre_push_ssh_auth` hook
104 self.server.update_environment(action=action, extras=extras)
105 self.create_hooks_env()
106 return os.system(self.command())
107
108
109 class MercurialServer(VcsServer):
110 backend = 'hg'
111
112 def __init__(self, store, ini_path, repo_name,
113 user, user_permissions, config, env):
114 super(MercurialServer, self).\
115 __init__(user, user_permissions, config, env)
116
117 self.store = store
118 self.ini_path = ini_path
119 self.repo_name = repo_name
120 self._path = self.hg_path = config.get(
121 'app:main', 'ssh.executable.hg')
122
123 self.tunnel = MercurialTunnelWrapper(server=self)
@@ -0,0 +1,228 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 logging
25 import signal
26 import tempfile
27 from subprocess import Popen, PIPE
28 import urlparse
29
30 from .base import VcsServer
31
32 log = logging.getLogger(__name__)
33
34
35 class SubversionTunnelWrapper(object):
36 process = None
37
38 def __init__(self, server):
39 self.server = server
40 self.timeout = 30
41 self.stdin = sys.stdin
42 self.stdout = sys.stdout
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45
46 self.read_only = True # flag that we set to make the hooks readonly
47
48 def create_svn_config(self):
49 content = (
50 '[general]\n'
51 'hooks-env = {}\n').format(self.hooks_env_path)
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 config_file.write(content)
54
55 def create_hooks_env(self):
56 content = (
57 '[default]\n'
58 'LANG = en_US.UTF-8\n')
59 if self.read_only:
60 content += 'SSH_READ_ONLY = 1\n'
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 hooks_env_file.write(content)
63
64 def remove_configs(self):
65 os.remove(self.svn_conf_path)
66 os.remove(self.hooks_env_path)
67
68 def command(self):
69 root = self.server.get_root_store()
70 command = [
71 self.server.svn_path, '-t',
72 '--config-file', self.svn_conf_path,
73 '-r', root]
74 log.debug("Final CMD: %s", command)
75 return command
76
77 def start(self):
78 command = self.command()
79 self.process = Popen(command, stdin=PIPE)
80
81 def sync(self):
82 while self.process.poll() is None:
83 next_byte = self.stdin.read(1)
84 if not next_byte:
85 break
86 self.process.stdin.write(next_byte)
87 self.remove_configs()
88
89 @property
90 def return_code(self):
91 return self.process.returncode
92
93 def get_first_client_response(self):
94 signal.signal(signal.SIGALRM, self.interrupt)
95 signal.alarm(self.timeout)
96 first_response = self._read_first_client_response()
97 signal.alarm(0)
98 return (
99 self._parse_first_client_response(first_response)
100 if first_response else None)
101
102 def patch_first_client_response(self, response, **kwargs):
103 self.create_hooks_env()
104 data = response.copy()
105 data.update(kwargs)
106 data['url'] = self._svn_string(data['url'])
107 data['ra_client'] = self._svn_string(data['ra_client'])
108 data['client'] = data['client'] or ''
109 buffer_ = (
110 "( {version} ( {capabilities} ) {url}{ra_client}"
111 "( {client}) ) ".format(**data))
112 self.process.stdin.write(buffer_)
113
114 def fail(self, message):
115 print(
116 "( failure ( ( 210005 {message} 0: 0 ) ) )".format(
117 message=self._svn_string(message)))
118 self.remove_configs()
119 self.process.kill()
120
121 def interrupt(self, signum, frame):
122 self.fail("Exited by timeout")
123
124 def _svn_string(self, str_):
125 if not str_:
126 return ''
127 return '{length}:{string} '.format(length=len(str_), string=str_)
128
129 def _read_first_client_response(self):
130 buffer_ = ""
131 brackets_stack = []
132 while True:
133 next_byte = self.stdin.read(1)
134 buffer_ += next_byte
135 if next_byte == "(":
136 brackets_stack.append(next_byte)
137 elif next_byte == ")":
138 brackets_stack.pop()
139 elif next_byte == " " and not brackets_stack:
140 break
141 return buffer_
142
143 def _parse_first_client_response(self, buffer_):
144 """
145 According to the Subversion RA protocol, the first request
146 should look like:
147
148 ( version:number ( cap:word ... ) url:string ? ra-client:string
149 ( ? client:string ) )
150
151 Please check https://svn.apache.org/repos/asf/subversion/trunk/
152 subversion/libsvn_ra_svn/protocol
153 """
154 version_re = r'(?P<version>\d+)'
155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
156 url_re = r'\d+\:(?P<url>[\W\w]+)'
157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
159 regex = re.compile(
160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
161 r'\(\s{client}\)\s\)\s*$'.format(
162 version=version_re, capabilities=capabilities_re,
163 url=url_re, ra_client=ra_client_re, client=client_re))
164 matcher = regex.match(buffer_)
165 return matcher.groupdict() if matcher else None
166
167 def run(self, extras):
168 action = 'pull'
169 self.create_svn_config()
170 self.start()
171
172 first_response = self.get_first_client_response()
173 if not first_response:
174 self.fail("Repository name cannot be extracted")
175 return 1
176
177 url_parts = urlparse.urlparse(first_response['url'])
178 self.server.repo_name = url_parts.path.strip('/')
179
180 exit_code = self.server._check_permissions(action)
181 if exit_code:
182 return exit_code
183
184 # set the readonly flag to False if we have proper permissions
185 if self.server.has_write_perm():
186 self.read_only = False
187 self.server.update_environment(action=action, extras=extras)
188
189 self.patch_first_client_response(first_response)
190 self.sync()
191 return self.return_code
192
193
194 class SubversionServer(VcsServer):
195 backend = 'svn'
196
197 def __init__(self, store, ini_path, repo_name,
198 user, user_permissions, config, env):
199 super(SubversionServer, self)\
200 .__init__(user, user_permissions, config, env)
201 self.store = store
202 self.ini_path = ini_path
203 # this is set in .run() from input stream
204 self.repo_name = repo_name
205 self._path = self.svn_path = config.get(
206 'app:main', 'ssh.executable.svn')
207
208 self.tunnel = SubversionTunnelWrapper(server=self)
209
210 def _handle_tunnel(self, extras):
211
212 # pre-auth
213 action = 'pull'
214 # Special case for SVN, we extract repo name at later stage
215 # exit_code = self._check_permissions(action)
216 # if exit_code:
217 # return exit_code, False
218
219 req = self.env['request']
220 server_url = req.host_url + req.script_name
221 extras['server_url'] = server_url
222
223 log.debug('Using %s binaries from path %s', self.backend, self._path)
224 exit_code = self.tunnel.run(extras)
225
226 return exit_code, action == "push"
227
228
@@ -0,0 +1,62 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 pytest
23 import ConfigParser
24
25 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper
26 from rhodecode.lib.utils2 import AttributeDict
27
28
29 @pytest.fixture
30 def dummy_conf_file(tmpdir):
31 conf = ConfigParser.ConfigParser()
32 conf.add_section('app:main')
33 conf.set('app:main', 'ssh.executable.hg', '/usr/bin/hg')
34 conf.set('app:main', 'ssh.executable.git', '/usr/bin/git')
35 conf.set('app:main', 'ssh.executable.svn', '/usr/bin/svnserve')
36
37 f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini')
38 with open(f_path, 'wb') as f:
39 conf.write(f)
40
41 return os.path.join(f_path)
42
43
44 @pytest.fixture
45 def dummy_env():
46 return {
47 'request':
48 AttributeDict(host_url='http://localhost', script_name='/')
49 }
50
51
52 @pytest.fixture
53 def dummy_user():
54 return AttributeDict(username='test_user')
55
56
57 @pytest.fixture
58 def ssh_wrapper(app, dummy_conf_file, dummy_env):
59 conn_info = '127.0.0.1 22 10.0.0.1 443'
60 return SshWrapper(
61 'random command', conn_info, 'auto', 'admin', '1', key_id='1',
62 shell=False, ini_path=dummy_conf_file, env=dummy_env)
@@ -67,12 +67,14 b' def main(ini_path, mode, user, user_id, '
67 'Please make sure this is set and available during execution '
67 'Please make sure this is set and available during execution '
68 'of this script.')
68 'of this script.')
69 connection_info = os.environ.get('SSH_CONNECTION', '')
69 connection_info = os.environ.get('SSH_CONNECTION', '')
70 request = Request.blank('/', base_url='http://rhodecode-ssh-wrapper/')
70
71 # TODO(marcink): configure the running host...
72 request = Request.blank('/', base_url='http://localhost:8080')
71 with bootstrap(ini_path, request=request) as env:
73 with bootstrap(ini_path, request=request) as env:
72 try:
74 try:
73 ssh_wrapper = SshWrapper(
75 ssh_wrapper = SshWrapper(
74 command, connection_info, mode,
76 command, connection_info, mode,
75 user, user_id, key_id, shell, ini_path)
77 user, user_id, key_id, shell, ini_path, env)
76 except Exception:
78 except Exception:
77 log.exception('Failed to execute SshWrapper')
79 log.exception('Failed to execute SshWrapper')
78 sys.exit(-5)
80 sys.exit(-5)
@@ -19,88 +19,100 b''
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import json
21 import json
22
22 import mock
23 import pytest
23 import pytest
24 from mock import Mock, patch
25
24
26 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
25 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
27
26 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
28
29 @pytest.fixture
30 def git_server():
31 return GitServerCreator()
32
27
33
28
34 class GitServerCreator(object):
29 class GitServerCreator(object):
35 root = '/tmp/repo/path/'
30 root = '/tmp/repo/path/'
36 git_path = '/usr/local/bin/'
31 git_path = '/usr/local/bin/git'
37 config_data = {
32 config_data = {
38 'app:main': {
33 'app:main': {
39 'ssh.executable.git': git_path
34 'ssh.executable.git': git_path,
35 'vcs.hooks.protocol': 'http',
40 }
36 }
41 }
37 }
42 repo_name = 'test_git'
38 repo_name = 'test_git'
43 repo_mode = 'receive-pack'
39 repo_mode = 'receive-pack'
44 user = 'vcs'
40 user = dummy_user()
45
41
46 def __init__(self):
42 def __init__(self):
47 def config_get(part, key):
43 def config_get(part, key):
48 return self.config_data.get(part, {}).get(key)
44 return self.config_data.get(part, {}).get(key)
49 self.config_mock = Mock()
45 self.config_mock = mock.Mock()
50 self.config_mock.get = Mock(side_effect=config_get)
46 self.config_mock.get = mock.Mock(side_effect=config_get)
51
47
52 def create(self, **kwargs):
48 def create(self, **kwargs):
53 parameters = {
49 parameters = {
54 'store': {'path': self.root},
50 'store': self.root,
55 'ini_path': '',
51 'ini_path': '',
56 'user': self.user,
52 'user': self.user,
57 'repo_name': self.repo_name,
53 'repo_name': self.repo_name,
58 'repo_mode': self.repo_mode,
54 'repo_mode': self.repo_mode,
59 'user_permissions': {
55 'user_permissions': {
60 self.repo_name: 'repo_admin'
56 self.repo_name: 'repository.admin'
61 },
57 },
62 'config': self.config_mock,
58 'config': self.config_mock,
59 'env': dummy_env()
63 }
60 }
64 parameters.update(kwargs)
61 parameters.update(kwargs)
65 server = GitServer(**parameters)
62 server = GitServer(**parameters)
66 return server
63 return server
67
64
68
65
66 @pytest.fixture
67 def git_server(app):
68 return GitServerCreator()
69
70
69 class TestGitServer(object):
71 class TestGitServer(object):
72
70 def test_command(self, git_server):
73 def test_command(self, git_server):
71 server = git_server.create()
74 server = git_server.create()
72 server.read_only = False
73 expected_command = (
75 expected_command = (
74 'cd {root}; {git_path}-{repo_mode}'
76 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
75 ' \'{root}{repo_name}\''.format(
76 root=git_server.root, git_path=git_server.git_path,
77 root=git_server.root, git_path=git_server.git_path,
77 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
78 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
78 )
79 )
79 assert expected_command == server.command
80 assert expected_command == server.tunnel.command()
81
82 @pytest.mark.parametrize('permissions, action, code', [
83 ({}, 'pull', -2),
84 ({'test_git': 'repository.read'}, 'pull', 0),
85 ({'test_git': 'repository.read'}, 'push', -2),
86 ({'test_git': 'repository.write'}, 'push', 0),
87 ({'test_git': 'repository.admin'}, 'push', 0),
80
88
81 def test_run_returns_exit_code_2_when_no_permissions(self, git_server, caplog):
89 ])
90 def test_permission_checks(self, git_server, permissions, action, code):
91 server = git_server.create(user_permissions=permissions)
92 result = server._check_permissions(action)
93 assert result is code
94
95 @pytest.mark.parametrize('permissions, value', [
96 ({}, False),
97 ({'test_git': 'repository.read'}, False),
98 ({'test_git': 'repository.write'}, True),
99 ({'test_git': 'repository.admin'}, True),
100
101 ])
102 def test_has_write_permissions(self, git_server, permissions, value):
103 server = git_server.create(user_permissions=permissions)
104 result = server.has_write_perm()
105 assert result is value
106
107 def test_run_returns_executes_command(self, git_server):
82 server = git_server.create()
108 server = git_server.create()
83 with patch.object(server, '_check_permissions') as permissions_mock:
109 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
84 with patch.object(server, '_update_environment'):
110 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
85 permissions_mock.return_value = 2
111 _patch.return_value = 0
112 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
86 exit_code = server.run()
113 exit_code = server.run()
87
114
88 assert exit_code == (2, False)
115 assert exit_code == (0, 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
116
105 @pytest.mark.parametrize(
117 @pytest.mark.parametrize(
106 'repo_mode, action', [
118 'repo_mode, action', [
@@ -109,88 +121,25 b' class TestGitServer(object):'
109 ])
121 ])
110 def test_update_environment(self, git_server, repo_mode, action):
122 def test_update_environment(self, git_server, repo_mode, action):
111 server = git_server.create(repo_mode=repo_mode)
123 server = git_server.create(repo_mode=repo_mode)
112 with patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
124 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
113 with patch('os.putenv') as putenv_mock:
125 with mock.patch('os.putenv') as putenv_mock:
114 server._update_environment()
126 server.update_environment(action)
115
127
116 expected_data = {
128 expected_data = {
117 "username": git_server.user,
129 'username': git_server.user.username,
118 "scm": "git",
130 'scm': 'git',
119 "repository": git_server.repo_name,
131 'repository': git_server.repo_name,
120 "make_lock": None,
132 'make_lock': None,
121 "action": [action],
133 'action': action,
122 "ip": "10.10.10.10",
134 'ip': '10.10.10.10',
123 "locked_by": [None, None],
135 'locked_by': [None, None],
124 "config": ""
136 'config': '',
137 'server_url': None,
138 'hooks': ['push', 'pull'],
139 'is_shadow_repo': False,
140 'hooks_module': 'rhodecode.lib.hooks_daemon',
141 'SSH': True,
142 'SSH_PERMISSIONS': 'repository.admin',
125 }
143 }
126 args, kwargs = putenv_mock.call_args
144 args, kwargs = putenv_mock.call_args
127 assert json.loads(args[1]) == expected_data
145 assert json.loads(args[1]) == expected_data
128
129
130 class TestGitServerCheckPermissions(object):
131 def test_returns_2_when_no_permissions_found(self, git_server, caplog):
132 user_permissions = {}
133 server = git_server.create(user_permissions=user_permissions)
134 result = server._check_permissions()
135 assert result == 2
136
137 log_msg = 'permission for vcs on test_git are: None'
138 assert log_msg in [t[2] for t in caplog.record_tuples]
139
140 def test_returns_2_when_no_permissions(self, git_server, caplog):
141 user_permissions = {git_server.repo_name: 'repository.none'}
142 server = git_server.create(user_permissions=user_permissions)
143 result = server._check_permissions()
144 assert result == 2
145
146 log_msg = 'repo not found or no permissions'
147 assert log_msg in [t[2] for t in caplog.record_tuples]
148
149 @pytest.mark.parametrize(
150 'permission', ['repository.admin', 'repository.write'])
151 def test_access_allowed_when_user_has_write_permissions(
152 self, git_server, permission, caplog):
153 user_permissions = {git_server.repo_name: permission}
154 server = git_server.create(user_permissions=user_permissions)
155 result = server._check_permissions()
156 assert result is None
157
158 log_msg = 'Write Permissions for User "%s" granted to repo "%s"!' % (
159 git_server.user, git_server.repo_name)
160 assert log_msg in [t[2] for t in caplog.record_tuples]
161
162 def test_write_access_is_not_allowed_when_user_has_read_permission(
163 self, git_server, caplog):
164 user_permissions = {git_server.repo_name: 'repository.read'}
165 server = git_server.create(
166 user_permissions=user_permissions, repo_mode='receive-pack')
167 result = server._check_permissions()
168 assert result == -3
169
170 log_msg = 'Only Read Only access for User "%s" granted to repo "%s"! Failing!' % (
171 git_server.user, git_server.repo_name)
172 assert log_msg in [t[2] for t in caplog.record_tuples]
173
174 def test_read_access_allowed_when_user_has_read_permission(
175 self, git_server, caplog):
176 user_permissions = {git_server.repo_name: 'repository.read'}
177 server = git_server.create(
178 user_permissions=user_permissions, repo_mode='upload-pack')
179 result = server._check_permissions()
180 assert result is None
181
182 log_msg = 'Only Read Only access for User "%s" granted to repo "%s"!' % (
183 git_server.user, git_server.repo_name)
184 assert log_msg in [t[2] for t in caplog.record_tuples]
185
186 def test_returns_error_when_permission_not_recognised(
187 self, git_server, caplog):
188 user_permissions = {git_server.repo_name: 'repository.whatever'}
189 server = git_server.create(
190 user_permissions=user_permissions, repo_mode='upload-pack')
191 result = server._check_permissions()
192 assert result == -2
193
194 log_msg = 'Cannot properly fetch user permission. ' \
195 'Return value is: repository.whatever'
196 assert log_msg in [t[2] for t in caplog.record_tuples] No newline at end of file
@@ -18,15 +18,11 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import pytest
22 import pytest
22 from mock import Mock, patch
23
23
24 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialServer
24 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialServer
25
25 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
26
27 @pytest.fixture
28 def hg_server():
29 return MercurialServerCreator()
30
26
31
27
32 class MercurialServerCreator(object):
28 class MercurialServerCreator(object):
@@ -35,112 +31,86 b' class MercurialServerCreator(object):'
35
31
36 config_data = {
32 config_data = {
37 'app:main': {
33 'app:main': {
38 'ssh.executable.hg': hg_path
34 'ssh.executable.hg': hg_path,
35 'vcs.hooks.protocol': 'http',
39 }
36 }
40 }
37 }
41 repo_name = 'test_hg'
38 repo_name = 'test_hg'
42 user = 'vcs'
39 user = dummy_user()
43
40
44 def __init__(self):
41 def __init__(self):
45 def config_get(part, key):
42 def config_get(part, key):
46 return self.config_data.get(part, {}).get(key)
43 return self.config_data.get(part, {}).get(key)
47 self.config_mock = Mock()
44 self.config_mock = mock.Mock()
48 self.config_mock.get = Mock(side_effect=config_get)
45 self.config_mock.get = mock.Mock(side_effect=config_get)
49
46
50 def create(self, **kwargs):
47 def create(self, **kwargs):
51 parameters = {
48 parameters = {
52 'store': {'path': self.root},
49 'store': self.root,
53 'ini_path': '',
50 'ini_path': '',
54 'user': self.user,
51 'user': self.user,
55 'repo_name': self.repo_name,
52 'repo_name': self.repo_name,
56 'user_permissions': {
53 'user_permissions': {
57 'test_hg': 'repo_admin'
54 'test_hg': 'repository.admin'
58 },
55 },
59 'config': self.config_mock,
56 'config': self.config_mock,
57 'env': dummy_env()
60 }
58 }
61 parameters.update(kwargs)
59 parameters.update(kwargs)
62 server = MercurialServer(**parameters)
60 server = MercurialServer(**parameters)
63 return server
61 return server
64
62
65
63
64 @pytest.fixture
65 def hg_server(app):
66 return MercurialServerCreator()
67
68
66 class TestMercurialServer(object):
69 class TestMercurialServer(object):
67 def test_read_only_command(self, hg_server):
70
71 def test_command(self, hg_server):
68 server = hg_server.create()
72 server = hg_server.create()
69 server.read_only = True
70 expected_command = (
73 expected_command = (
71 'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio'
74 'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio'.format(
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,
75 root=hg_server.root, hg_path=hg_server.hg_path,
84 repo_name=hg_server.repo_name)
76 repo_name=hg_server.repo_name)
85 )
77 )
86 assert expected_command == server.command
78 assert expected_command == server.tunnel.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
79
97 def test_access_rejected_when_no_permissions(self, hg_server, caplog):
80 @pytest.mark.parametrize('permissions, action, code', [
98 user_permissions = {hg_server.repo_name: 'repository.none'}
81 ({}, 'pull', -2),
99 server = hg_server.create(user_permissions=user_permissions)
82 ({'test_hg': 'repository.read'}, 'pull', 0),
100 result = server._check_permissions()
83 ({'test_hg': 'repository.read'}, 'push', -2),
101 assert result is False
84 ({'test_hg': 'repository.write'}, 'push', 0),
102
85 ({'test_hg': 'repository.admin'}, 'push', 0),
103 log_msg = 'repo not found or no permissions'
104 assert log_msg in [t[2] for t in caplog.record_tuples]
105
86
106 @pytest.mark.parametrize(
87 ])
107 'permission', ['repository.admin', 'repository.write'])
88 def test_permission_checks(self, hg_server, permissions, action, code):
108 def test_access_allowed_when_user_has_write_permissions(
89 server = hg_server.create(user_permissions=permissions)
109 self, hg_server, permission, caplog):
90 result = server._check_permissions(action)
110 user_permissions = {hg_server.repo_name: permission}
91 assert result is code
111 server = hg_server.create(user_permissions=user_permissions)
112 result = server._check_permissions()
113 assert result is True
114
92
115 assert server.read_only is False
93 @pytest.mark.parametrize('permissions, value', [
116 log_msg = 'Write Permissions for User "vcs" granted to repo "test_hg"!'
94 ({}, False),
117 assert log_msg in [t[2] for t in caplog.record_tuples]
95 ({'test_hg': 'repository.read'}, False),
118
96 ({'test_hg': 'repository.write'}, True),
119 def test_access_allowed_when_user_has_read_permissions(self, hg_server, caplog):
97 ({'test_hg': 'repository.admin'}, True),
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
98
125 assert server.read_only is True
99 ])
126 log_msg = 'Only Read Only access for User "%s" granted to repo "%s"!' % (
100 def test_has_write_permissions(self, hg_server, permissions, value):
127 hg_server.user, hg_server.repo_name)
101 server = hg_server.create(user_permissions=permissions)
128 assert log_msg in [t[2] for t in caplog.record_tuples]
102 result = server.has_write_perm()
103 assert result is value
129
104
130 def test_run_returns_exit_code_2_when_no_permissions(self, hg_server, caplog):
105 def test_run_returns_executes_command(self, hg_server):
131 server = hg_server.create()
106 server = hg_server.create()
132 with patch.object(server, '_check_permissions') as permissions_mock:
107 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialTunnelWrapper
133 permissions_mock.return_value = False
108 with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch:
134 exit_code = server.run()
109 _patch.return_value = 0
135 assert exit_code == (2, False)
110 with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'):
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()
111 exit_code = server.run()
144
112
145 system_mock.assert_called_once_with(server.command)
146 assert exit_code == (0, False)
113 assert exit_code == (0, False)
114
115
116
@@ -18,15 +18,11 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import pytest
22 import pytest
22 from mock import Mock, patch
23
23
24 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
24 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
25
25 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
26
27 @pytest.fixture
28 def svn_server():
29 return SubversionServerCreator()
30
26
31
27
32 class SubversionServerCreator(object):
28 class SubversionServerCreator(object):
@@ -34,103 +30,95 b' class SubversionServerCreator(object):'
34 svn_path = '/usr/local/bin/svnserve'
30 svn_path = '/usr/local/bin/svnserve'
35 config_data = {
31 config_data = {
36 'app:main': {
32 'app:main': {
37 'ssh.executable.svn': svn_path
33 'ssh.executable.svn': svn_path,
34 'vcs.hooks.protocol': 'http',
38 }
35 }
39 }
36 }
40 repo_name = 'test-svn'
37 repo_name = 'test-svn'
41 user = 'vcs'
38 user = dummy_user()
42
39
43 def __init__(self):
40 def __init__(self):
44 def config_get(part, key):
41 def config_get(part, key):
45 return self.config_data.get(part, {}).get(key)
42 return self.config_data.get(part, {}).get(key)
46 self.config_mock = Mock()
43 self.config_mock = mock.Mock()
47 self.config_mock.get = Mock(side_effect=config_get)
44 self.config_mock.get = mock.Mock(side_effect=config_get)
48
45
49 def create(self, **kwargs):
46 def create(self, **kwargs):
50 parameters = {
47 parameters = {
51 'store': {'path': self.root},
48 'store': self.root,
49 'repo_name': self.repo_name,
52 'ini_path': '',
50 'ini_path': '',
53 'user': self.user,
51 'user': self.user,
54 'user_permissions': {
52 'user_permissions': {
55 self.repo_name: 'repo_admin'
53 self.repo_name: 'repository.admin'
56 },
54 },
57 'config': self.config_mock,
55 'config': self.config_mock,
56 'env': dummy_env()
58 }
57 }
58
59 parameters.update(kwargs)
59 parameters.update(kwargs)
60 server = SubversionServer(**parameters)
60 server = SubversionServer(**parameters)
61 return server
61 return server
62
62
63
63
64 @pytest.fixture
65 def svn_server(app):
66 return SubversionServerCreator()
67
68
64 class TestSubversionServer(object):
69 class TestSubversionServer(object):
65 def test_timeout_returns_value_from_config(self, svn_server):
70 def test_command(self, svn_server):
66 server = svn_server.create()
71 server = svn_server.create()
67 assert server.timeout == 30
72 expected_command = [
73 svn_server.svn_path, '-t', '--config-file',
74 server.tunnel.svn_conf_path, '-r', svn_server.root
75 ]
68
76
69 @pytest.mark.parametrize(
77 assert expected_command == server.tunnel.command()
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
78
81 def test_check_permissions_with_read_permissions(self, svn_server):
79 @pytest.mark.parametrize('permissions, action, code', [
82 user_permissions = {svn_server.repo_name: 'repository.read'}
80 ({}, 'pull', -2),
83 server = svn_server.create(user_permissions=user_permissions)
81 ({'test-svn': 'repository.read'}, 'pull', 0),
84 server.tunnel = Mock()
82 ({'test-svn': 'repository.read'}, 'push', -2),
85 server.repo_name = svn_server.repo_name
83 ({'test-svn': 'repository.write'}, 'push', 0),
86 result = server._check_permissions()
84 ({'test-svn': 'repository.admin'}, 'push', 0),
87 assert result is True
85
88 assert server.tunnel.read_only is True
86 ])
87 def test_permission_checks(self, svn_server, permissions, action, code):
88 server = svn_server.create(user_permissions=permissions)
89 result = server._check_permissions(action)
90 assert result is code
89
91
90 def test_check_permissions_with_no_permissions(self, svn_server, caplog):
92 def test_run_returns_executes_command(self, svn_server):
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()
93 server = svn_server.create()
114 fake_response = {
94 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
115 'url': 'ssh+svn://test@example.com/test-svn/'
95 with mock.patch.object(
116 }
96 SubversionTunnelWrapper, 'get_first_client_response',
117 with patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.SubversionTunnelWrapper') as tunnel_mock:
97 return_value={'url': 'http://server/test-svn'}):
118 with patch.object(server, '_check_permissions') as (
98 with mock.patch.object(
119 permissions_mock):
99 SubversionTunnelWrapper, 'patch_first_client_response',
120 permissions_mock.return_value = True
100 return_value=0):
121 tunnel = tunnel_mock()
101 with mock.patch.object(
122 tunnel.get_first_client_response.return_value = fake_response
102 SubversionTunnelWrapper, 'sync',
123 tunnel.return_code = 0
103 return_value=0):
124 exit_code = server.run()
104 with mock.patch.object(
125 permissions_mock.assert_called_once_with()
105 SubversionTunnelWrapper, 'command',
106 return_value='date'):
126
107
127 expected_log_calls = sorted([
108 exit_code = server.run()
128 "Using subversion binaries from '%s'" % svn_server.svn_path
109 # SVN has this differently configured, and we get in our mock env
129 ])
110 # None as return code
111 assert exit_code == (None, False)
130
112
131 assert expected_log_calls == [t[2] for t in caplog.record_tuples]
113 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
114 server = svn_server.create()
115 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
116 with mock.patch.object(
117 SubversionTunnelWrapper, 'command',
118 return_value='date'):
119 with mock.patch.object(
120 SubversionTunnelWrapper, 'get_first_client_response',
121 return_value=None):
122 exit_code = server.run()
132
123
133 assert exit_code == (0, False)
124 assert exit_code == (1, False)
134 tunnel.patch_first_client_response.assert_called_once_with(
135 fake_response)
136 tunnel.sync.assert_called_once_with()
@@ -17,185 +17,38 b''
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
20
21 import mock
22 import pytest
21 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 f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini')
37 with open(f_path, 'wb') as f:
38 conf.write(f)
39
40 return os.path.join(f_path)
41
42
43 class TestGetRepoDetails(object):
44 @pytest.mark.parametrize(
45 'command', [
46 'hg -R test-repo serve --stdio',
47 'hg -R test-repo serve --stdio'
48 ])
49 def test_hg_command_matched(self, command, dummy_conf):
50 wrapper = SshWrapper(command, 'auto', 'admin', '3', 'False', dummy_conf)
51 type_, name, mode = wrapper.get_repo_details('auto')
52 assert type_ == 'hg'
53 assert name == 'test-repo'
54 assert mode is 'auto'
55
56 @pytest.mark.parametrize(
57 'command', [
58 'hg test-repo serve --stdio',
59 'hg -R test-repo serve',
60 'hg serve --stdio',
61 'hg serve -R test-repo'
62 ])
63 def test_hg_command_not_matched(self, command, dummy_conf):
64 wrapper = SshWrapper(command, 'auto', 'admin', '3', 'False', dummy_conf)
65 type_, name, mode = wrapper.get_repo_details('auto')
66 assert type_ is None
67 assert name is None
68 assert mode is 'auto'
69
70
71 class TestServe(object):
72 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, dummy_conf):
73 with mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store'):
74 wrapper = SshWrapper('random command', 'auto', 'admin', '3', 'False', dummy_conf)
75
76 with pytest.raises(Exception) as exc_info:
77 wrapper.serve(
78 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
79 permissions={})
80 assert exc_info.value.message == 'Unrecognised VCS: microsoft-tfs'
81
22
82
23
83 class TestServeHg(object):
24 class TestSSHWrapper(object):
84
85 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
86 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
87 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
88 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.MercurialServer.run')
89 def test_serve_creates_hg_instance(
90 self, mercurial_run_mock, get_repo_store_mock, get_user_mock,
91 invalidate_cache_mock, dummy_conf):
92
93 repo_name = None
94 mercurial_run_mock.return_value = 0, True
95 get_user_mock.return_value = {repo_name: 'repository.admin'}
96 get_repo_store_mock.return_value = {'path': '/tmp'}
97
98 wrapper = SshWrapper('date', 'hg', 'admin', '3', 'False',
99 dummy_conf)
100 exit_code = wrapper.wrap()
101 assert exit_code == 0
102 assert mercurial_run_mock.called
103
104 assert get_repo_store_mock.called
105 assert get_user_mock.called
106 invalidate_cache_mock.assert_called_once_with(repo_name)
107
25
108 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
26 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, ssh_wrapper):
109 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
27 with pytest.raises(Exception) as exc_info:
110 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
28 ssh_wrapper.serve(
111 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.MercurialServer.run')
29 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
112 def test_serve_hg_invalidates_cache(
30 permissions={})
113 self, mercurial_run_mock, get_repo_store_mock, get_user_mock,
31 assert exc_info.value.message == 'Unrecognised VCS: microsoft-tfs'
114 invalidate_cache_mock, dummy_conf):
115
116 repo_name = None
117 mercurial_run_mock.return_value = 0, True
118 get_user_mock.return_value = {repo_name: 'repository.admin'}
119 get_repo_store_mock.return_value = {'path': '/tmp'}
120
32
121 wrapper = SshWrapper('date', 'hg', 'admin', '3', 'False',
33 def test_parse_config(self, ssh_wrapper):
122 dummy_conf)
34 config = ssh_wrapper.parse_config(ssh_wrapper.ini_path)
123 exit_code = wrapper.wrap()
35 assert config
124 assert exit_code == 0
125 assert mercurial_run_mock.called
126
127 assert get_repo_store_mock.called
128 assert get_user_mock.called
129 invalidate_cache_mock.assert_called_once_with(repo_name)
130
131
132 class TestServeGit(object):
133
36
134 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
37 def test_get_connection_info(self, ssh_wrapper):
135 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
38 conn_info = ssh_wrapper.get_connection_info()
136 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
39 assert {'client_ip': '127.0.0.1',
137 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.GitServer.run')
40 'client_port': '22',
138 def test_serve_creates_git_instance(self, git_run_mock, get_repo_store_mock, get_user_mock,
41 'server_ip': '10.0.0.1',
139 invalidate_cache_mock, dummy_conf):
42 'server_port': '443'} == conn_info
140 repo_name = None
141 git_run_mock.return_value = 0, True
142 get_user_mock.return_value = {repo_name: 'repository.admin'}
143 get_repo_store_mock.return_value = {'path': '/tmp'}
144
145 wrapper = SshWrapper('date', 'git', 'admin', '3', 'False',
146 dummy_conf)
147
148 exit_code = wrapper.wrap()
149 assert exit_code == 0
150 assert git_run_mock.called
151 assert get_repo_store_mock.called
152 assert get_user_mock.called
153 invalidate_cache_mock.assert_called_once_with(repo_name)
154
155 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
156 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
157 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
158 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.GitServer.run')
159 def test_serve_git_invalidates_cache(
160 self, git_run_mock, get_repo_store_mock, get_user_mock,
161 invalidate_cache_mock, dummy_conf):
162 repo_name = None
163 git_run_mock.return_value = 0, True
164 get_user_mock.return_value = {repo_name: 'repository.admin'}
165 get_repo_store_mock.return_value = {'path': '/tmp'}
166
43
167 wrapper = SshWrapper('date', 'git', 'admin', '3', 'False', dummy_conf)
44 @pytest.mark.parametrize('command, vcs', [
168
45 ('xxx', None),
169 exit_code = wrapper.wrap()
46 ('svnserve -t', 'svn'),
170 assert exit_code == 0
47 ('hg -R repo serve --stdio', 'hg'),
171 assert git_run_mock.called
48 ('git-receive-pack \'repo.git\'', 'git'),
172
173 assert get_repo_store_mock.called
174 assert get_user_mock.called
175 invalidate_cache_mock.assert_called_once_with(repo_name)
176
177
178 class TestServeSvn(object):
179
49
180 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
50 ])
181 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
51 def test_get_repo_details(self, ssh_wrapper, command, vcs):
182 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
52 ssh_wrapper.command = command
183 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.SubversionServer.run')
53 vcs_type, repo_name, mode = ssh_wrapper.get_repo_details(mode='auto')
184 def test_serve_creates_svn_instance(
54 assert vcs_type == vcs
185 self, svn_run_mock, get_repo_store_mock, get_user_mock,
186 invalidate_cache_mock, dummy_conf):
187
188 repo_name = None
189 svn_run_mock.return_value = 0, True
190 get_user_mock.return_value = {repo_name: 'repository.admin'}
191 get_repo_store_mock.return_value = {'path': '/tmp'}
192
193 wrapper = SshWrapper('date', 'svn', 'admin', '3', 'False', dummy_conf)
194
195 exit_code = wrapper.wrap()
196 assert exit_code == 0
197 assert svn_run_mock.called
198
199 assert get_repo_store_mock.called
200 assert get_user_mock.called
201 invalidate_cache_mock.assert_called_once_with(repo_name)
@@ -628,7 +628,6 b' def bootstrap_request(**kwargs):'
628 request = TestRequest(**kwargs)
628 request = TestRequest(**kwargs)
629 request.session = TestDummySession()
629 request.session = TestDummySession()
630
630
631
632 config = pyramid.testing.setUp(request=request)
631 config = pyramid.testing.setUp(request=request)
633 add_events_routes(config)
632 add_events_routes(config)
634 return request
633 return request
@@ -209,9 +209,9 b' class Hooks(object):'
209
209
210 def _call_hook(self, hook, extras):
210 def _call_hook(self, hook, extras):
211 extras = AttributeDict(extras)
211 extras = AttributeDict(extras)
212 server_url = extras['server_url']
212
213
213 extras.request = bootstrap_request(
214 extras.request = bootstrap_request(application_url=server_url)
214 application_url=extras['server_url'])
215
215
216 try:
216 try:
217 result = hook(extras)
217 result = hook(extras)
@@ -229,6 +229,7 b' class Hooks(object):'
229 finally:
229 finally:
230 meta.Session.remove()
230 meta.Session.remove()
231
231
232 log.debug('Got hook call response %s', result)
232 return {
233 return {
233 'status': result.status,
234 'status': result.status,
234 'output': result.output,
235 'output': result.output,
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now