##// END OF EJS Templates
svn: fixed case of wrong extracted repository name for SSH backend. In cases...
dan -
r4281:5da17e74 default
parent child Browse files
Show More
@@ -1,223 +1,220 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import re
23 23 import logging
24 24 import datetime
25 25 from pyramid.compat import configparser
26 26
27 27 from rhodecode.model.db import Session, User, UserSshKeys
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 from .hg import MercurialServer
31 31 from .git import GitServer
32 32 from .svn import SubversionServer
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class SshWrapper(object):
37 37
38 38 def __init__(self, command, connection_info, mode,
39 39 user, user_id, key_id, shell, ini_path, env):
40 40 self.command = command
41 41 self.connection_info = connection_info
42 42 self.mode = mode
43 43 self.user = user
44 44 self.user_id = user_id
45 45 self.key_id = key_id
46 46 self.shell = shell
47 47 self.ini_path = ini_path
48 48 self.env = env
49 49
50 50 self.config = self.parse_config(ini_path)
51 51 self.server_impl = None
52 52
53 53 def parse_config(self, config_path):
54 54 parser = configparser.ConfigParser()
55 55 parser.read(config_path)
56 56 return parser
57 57
58 58 def update_key_access_time(self, key_id):
59 59 key = UserSshKeys().query().filter(
60 60 UserSshKeys.ssh_key_id == key_id).scalar()
61 61 if key:
62 62 key.accessed_on = datetime.datetime.utcnow()
63 63 Session().add(key)
64 64 Session().commit()
65 65 log.debug('Update key id:`%s` fingerprint:`%s` access time',
66 66 key_id, key.ssh_key_fingerprint)
67 67
68 68 def get_connection_info(self):
69 69 """
70 70 connection_info
71 71
72 72 Identifies the client and server ends of the connection.
73 73 The variable contains four space-separated values: client IP address,
74 74 client port number, server IP address, and server port number.
75 75 """
76 76 conn = dict(
77 77 client_ip=None,
78 78 client_port=None,
79 79 server_ip=None,
80 80 server_port=None,
81 81 )
82 82
83 83 info = self.connection_info.split(' ')
84 84 if len(info) == 4:
85 85 conn['client_ip'] = info[0]
86 86 conn['client_port'] = info[1]
87 87 conn['server_ip'] = info[2]
88 88 conn['server_port'] = info[3]
89 89
90 90 return conn
91 91
92 92 def get_repo_details(self, mode):
93 93 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
94 mode = mode
95 94 repo_name = None
96 95
97 96 hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$'
98 97 hg_match = re.match(hg_pattern, self.command)
99 98 if hg_match is not None:
100 99 vcs_type = 'hg'
101 100 repo_name = hg_match.group(1).strip('/')
102 101 return vcs_type, repo_name, mode
103 102
104 git_pattern = (
105 r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
103 git_pattern = r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$'
106 104 git_match = re.match(git_pattern, self.command)
107 105 if git_match is not None:
108 106 vcs_type = 'git'
109 107 repo_name = git_match.group(2).strip('/')
110 108 mode = git_match.group(1)
111 109 return vcs_type, repo_name, mode
112 110
113 111 svn_pattern = r'^svnserve -t'
114 112 svn_match = re.match(svn_pattern, self.command)
115 113
116 114 if svn_match is not None:
117 115 vcs_type = 'svn'
118 # Repo name should be extracted from the input stream
116 # Repo name should be extracted from the input stream, we're unable to
117 # extract it at this point in execution
119 118 return vcs_type, repo_name, mode
120 119
121 120 return vcs_type, repo_name, mode
122 121
123 122 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
124 123 store = ScmModel().repos_path
125 124
126 125 check_branch_perms = False
127 126 detect_force_push = False
128 127
129 128 if branch_permissions:
130 129 check_branch_perms = True
131 130 detect_force_push = True
132 131
133 132 log.debug(
134 133 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
135 134 vcs, mode, repo, check_branch_perms)
136 135
137 136 # detect if we have to check branch permissions
138 137 extras = {
139 138 'detect_force_push': detect_force_push,
140 139 'check_branch_perms': check_branch_perms,
141 140 }
142 141
143 142 if vcs == 'hg':
144 143 server = MercurialServer(
145 144 store=store, ini_path=self.ini_path,
146 145 repo_name=repo, user=user,
147 146 user_permissions=permissions, config=self.config, env=self.env)
148 147 self.server_impl = server
149 148 return server.run(tunnel_extras=extras)
150 149
151 150 elif vcs == 'git':
152 151 server = GitServer(
153 152 store=store, ini_path=self.ini_path,
154 153 repo_name=repo, repo_mode=mode, user=user,
155 154 user_permissions=permissions, config=self.config, env=self.env)
156 155 self.server_impl = server
157 156 return server.run(tunnel_extras=extras)
158 157
159 158 elif vcs == 'svn':
160 159 server = SubversionServer(
161 160 store=store, ini_path=self.ini_path,
162 161 repo_name=None, user=user,
163 162 user_permissions=permissions, config=self.config, env=self.env)
164 163 self.server_impl = server
165 164 return server.run(tunnel_extras=extras)
166 165
167 166 else:
168 167 raise Exception('Unrecognised VCS: {}'.format(vcs))
169 168
170 169 def wrap(self):
171 170 mode = self.mode
172 171 user = self.user
173 172 user_id = self.user_id
174 173 key_id = self.key_id
175 174 shell = self.shell
176 175
177 176 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
178 177
179 178 log.debug(
180 179 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` '
181 180 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
182 181 mode, user, user_id, shell, self.command,
183 182 scm_detected, scm_mode, scm_repo)
184 183
185 184 # update last access time for this key
186 185 self.update_key_access_time(key_id)
187 186
188 187 log.debug('SSH Connection info %s', self.get_connection_info())
189 188
190 189 if shell and self.command is None:
191 log.info(
192 'Dropping to shell, no command given and shell is allowed')
190 log.info('Dropping to shell, no command given and shell is allowed')
193 191 os.execl('/bin/bash', '-l')
194 192 exit_code = 1
195 193
196 194 elif scm_detected:
197 195 user = User.get(user_id)
198 196 if not user:
199 197 log.warning('User with id %s not found', user_id)
200 198 exit_code = -1
201 199 return exit_code
202 200
203 201 auth_user = user.AuthUser()
204 202 permissions = auth_user.permissions['repositories']
205 203 repo_branch_permissions = auth_user.get_branch_permissions(scm_repo)
206 204 try:
207 205 exit_code, is_updated = self.serve(
208 206 scm_detected, scm_repo, scm_mode, user, permissions,
209 207 repo_branch_permissions)
210 208 except Exception:
211 209 log.exception('Error occurred during execution of SshWrapper')
212 210 exit_code = -1
213 211
214 212 elif self.command is None and shell is False:
215 213 log.error('No Command given.')
216 214 exit_code = -1
217 215
218 216 else:
219 log.error(
220 'Unhandled Command: "%s" Aborting.', self.command)
217 log.error('Unhandled Command: "%s" Aborting.', self.command)
221 218 exit_code = -1
222 219
223 220 return exit_code
@@ -1,163 +1,162 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import json
24 24 import logging
25 25
26 26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
27 27 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class VcsServer(object):
34 34 _path = None # set executable path for hg/git/svn binary
35 35 backend = None # set in child classes
36 36 tunnel = None # subprocess handling tunnel
37 37 write_perms = ['repository.admin', 'repository.write']
38 38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39 39
40 40 def __init__(self, user, user_permissions, config, env):
41 41 self.user = user
42 42 self.user_permissions = user_permissions
43 43 self.config = config
44 44 self.env = env
45 45 self.stdin = sys.stdin
46 46
47 47 self.repo_name = None
48 48 self.repo_mode = None
49 49 self.store = ''
50 50 self.ini_path = ''
51 51
52 52 def _invalidate_cache(self, repo_name):
53 53 """
54 54 Set's cache for this repository for invalidation on next access
55 55
56 56 :param repo_name: full repo name, also a cache key
57 57 """
58 58 ScmModel().mark_for_invalidation(repo_name)
59 59
60 60 def has_write_perm(self):
61 61 permission = self.user_permissions.get(self.repo_name)
62 62 if permission in ['repository.write', 'repository.admin']:
63 63 return True
64 64
65 65 return False
66 66
67 67 def _check_permissions(self, action):
68 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)
69 log.debug('permission for %s on %s are: %s',
70 self.user, self.repo_name, permission)
72 71
73 72 if not permission:
74 73 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
75 74 self.user, self.repo_name)
76 75 return -2
77 76
78 77 if action == 'pull':
79 78 if permission in self.read_perms:
80 79 log.info(
81 80 'READ Permissions for User "%s" detected to repo "%s"!',
82 81 self.user, self.repo_name)
83 82 return 0
84 83 else:
85 84 if permission in self.write_perms:
86 85 log.info(
87 86 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
88 87 self.user, self.repo_name)
89 88 return 0
90 89
91 90 log.error('Cannot properly fetch or verify user `%s` permissions. '
92 91 'Permissions: %s, vcs action: %s',
93 92 self.user, permission, action)
94 93 return -2
95 94
96 95 def update_environment(self, action, extras=None):
97 96
98 97 scm_data = {
99 98 'ip': os.environ['SSH_CLIENT'].split()[0],
100 99 'username': self.user.username,
101 100 'user_id': self.user.user_id,
102 101 'action': action,
103 102 'repository': self.repo_name,
104 103 'scm': self.backend,
105 104 'config': self.ini_path,
106 105 'repo_store': self.store,
107 106 'make_lock': None,
108 107 'locked_by': [None, None],
109 108 'server_url': None,
110 109 'user_agent': 'ssh-user-agent',
111 110 'hooks': ['push', 'pull'],
112 111 'hooks_module': 'rhodecode.lib.hooks_daemon',
113 112 'is_shadow_repo': False,
114 113 'detect_force_push': False,
115 114 'check_branch_perms': False,
116 115
117 116 'SSH': True,
118 117 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
119 118 }
120 119 if extras:
121 120 scm_data.update(extras)
122 121 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
123 122
124 123 def get_root_store(self):
125 124 root_store = self.store
126 125 if not root_store.endswith('/'):
127 126 # always append trailing slash
128 127 root_store = root_store + '/'
129 128 return root_store
130 129
131 130 def _handle_tunnel(self, extras):
132 131 # pre-auth
133 132 action = 'pull'
134 133 exit_code = self._check_permissions(action)
135 134 if exit_code:
136 135 return exit_code, False
137 136
138 137 req = self.env['request']
139 138 server_url = req.host_url + req.script_name
140 139 extras['server_url'] = server_url
141 140
142 141 log.debug('Using %s binaries from path %s', self.backend, self._path)
143 142 exit_code = self.tunnel.run(extras)
144 143
145 144 return exit_code, action == "push"
146 145
147 146 def run(self, tunnel_extras=None):
148 147 tunnel_extras = tunnel_extras or {}
149 148 extras = {}
150 149 extras.update(tunnel_extras)
151 150
152 151 callback_daemon, extras = prepare_callback_daemon(
153 152 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
154 153 host=vcs_settings.HOOKS_HOST,
155 154 use_direct_calls=False)
156 155
157 156 with callback_daemon:
158 157 try:
159 158 return self._handle_tunnel(extras)
160 159 finally:
161 160 log.debug('Running cleanup with cache invalidation')
162 161 if self.repo_name:
163 162 self._invalidate_cache(self.repo_name)
@@ -1,75 +1,74 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24
25 25 from .base import VcsServer
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 class GitTunnelWrapper(object):
31 31 process = None
32 32
33 33 def __init__(self, server):
34 34 self.server = server
35 35 self.stdin = sys.stdin
36 36 self.stdout = sys.stdout
37 37
38 38 def create_hooks_env(self):
39 39 pass
40 40
41 41 def command(self):
42 42 root = self.server.get_root_store()
43 43 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
44 44 root=root, git_path=self.server.git_path,
45 45 mode=self.server.repo_mode, repo_name=self.server.repo_name)
46 46 log.debug("Final CMD: %s", command)
47 47 return command
48 48
49 49 def run(self, extras):
50 50 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
51 51 exit_code = self.server._check_permissions(action)
52 52 if exit_code:
53 53 return exit_code
54 54
55 55 self.server.update_environment(action=action, extras=extras)
56 56 self.create_hooks_env()
57 57 return os.system(self.command())
58 58
59 59
60 60 class GitServer(VcsServer):
61 61 backend = 'git'
62 62
63 63 def __init__(self, store, ini_path, repo_name, repo_mode,
64 64 user, user_permissions, config, env):
65 65 super(GitServer, self).\
66 66 __init__(user, user_permissions, config, env)
67 67
68 68 self.store = store
69 69 self.ini_path = ini_path
70 70 self.repo_name = repo_name
71 self._path = self.git_path = config.get(
72 'app:main', 'ssh.executable.git')
71 self._path = self.git_path = config.get('app:main', 'ssh.executable.git')
73 72
74 73 self.repo_mode = repo_mode
75 74 self.tunnel = GitTunnelWrapper(server=self)
@@ -1,228 +1,254 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import re
23 23 import sys
24 24 import logging
25 25 import signal
26 26 import tempfile
27 27 from subprocess import Popen, PIPE
28 28 import urlparse
29 29
30 30 from .base import VcsServer
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class SubversionTunnelWrapper(object):
36 36 process = None
37 37
38 38 def __init__(self, server):
39 39 self.server = server
40 40 self.timeout = 30
41 41 self.stdin = sys.stdin
42 42 self.stdout = sys.stdout
43 43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45 45
46 46 self.read_only = True # flag that we set to make the hooks readonly
47 47
48 48 def create_svn_config(self):
49 49 content = (
50 50 '[general]\n'
51 51 'hooks-env = {}\n').format(self.hooks_env_path)
52 52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 53 config_file.write(content)
54 54
55 55 def create_hooks_env(self):
56 56 content = (
57 57 '[default]\n'
58 58 'LANG = en_US.UTF-8\n')
59 59 if self.read_only:
60 60 content += 'SSH_READ_ONLY = 1\n'
61 61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 62 hooks_env_file.write(content)
63 63
64 64 def remove_configs(self):
65 65 os.remove(self.svn_conf_path)
66 66 os.remove(self.hooks_env_path)
67 67
68 68 def command(self):
69 69 root = self.server.get_root_store()
70 70 command = [
71 71 self.server.svn_path, '-t',
72 72 '--config-file', self.svn_conf_path,
73 73 '-r', root]
74 74 log.debug("Final CMD: %s", ' '.join(command))
75 75 return command
76 76
77 77 def start(self):
78 78 command = self.command()
79 79 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
80 80
81 81 def sync(self):
82 82 while self.process.poll() is None:
83 83 next_byte = self.stdin.read(1)
84 84 if not next_byte:
85 85 break
86 86 self.process.stdin.write(next_byte)
87 87 self.remove_configs()
88 88
89 89 @property
90 90 def return_code(self):
91 91 return self.process.returncode
92 92
93 93 def get_first_client_response(self):
94 94 signal.signal(signal.SIGALRM, self.interrupt)
95 95 signal.alarm(self.timeout)
96 96 first_response = self._read_first_client_response()
97 97 signal.alarm(0)
98 return (
99 self._parse_first_client_response(first_response)
100 if first_response else None)
98 return (self._parse_first_client_response(first_response)
99 if first_response else None)
101 100
102 101 def patch_first_client_response(self, response, **kwargs):
103 102 self.create_hooks_env()
104 103 data = response.copy()
105 104 data.update(kwargs)
106 105 data['url'] = self._svn_string(data['url'])
107 106 data['ra_client'] = self._svn_string(data['ra_client'])
108 107 data['client'] = data['client'] or ''
109 108 buffer_ = (
110 109 "( {version} ( {capabilities} ) {url}{ra_client}"
111 110 "( {client}) ) ".format(**data))
112 111 self.process.stdin.write(buffer_)
113 112
114 113 def fail(self, message):
115 print(
116 "( failure ( ( 210005 {message} 0: 0 ) ) )".format(
117 message=self._svn_string(message)))
114 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
115 message=self._svn_string(message)))
118 116 self.remove_configs()
119 117 self.process.kill()
120 118 return 1
121 119
122 120 def interrupt(self, signum, frame):
123 121 self.fail("Exited by timeout")
124 122
125 123 def _svn_string(self, str_):
126 124 if not str_:
127 125 return ''
128 126 return '{length}:{string} '.format(length=len(str_), string=str_)
129 127
130 128 def _read_first_client_response(self):
131 129 buffer_ = ""
132 130 brackets_stack = []
133 131 while True:
134 132 next_byte = self.stdin.read(1)
135 133 buffer_ += next_byte
136 134 if next_byte == "(":
137 135 brackets_stack.append(next_byte)
138 136 elif next_byte == ")":
139 137 brackets_stack.pop()
140 138 elif next_byte == " " and not brackets_stack:
141 139 break
140
142 141 return buffer_
143 142
144 143 def _parse_first_client_response(self, buffer_):
145 144 """
146 145 According to the Subversion RA protocol, the first request
147 146 should look like:
148 147
149 148 ( version:number ( cap:word ... ) url:string ? ra-client:string
150 149 ( ? client:string ) )
151 150
152 Please check https://svn.apache.org/repos/asf/subversion/trunk/
153 subversion/libsvn_ra_svn/protocol
151 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
154 152 """
155 153 version_re = r'(?P<version>\d+)'
156 154 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
157 155 url_re = r'\d+\:(?P<url>[\W\w]+)'
158 156 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
159 157 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
160 158 regex = re.compile(
161 159 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
162 160 r'\(\s{client}\)\s\)\s*$'.format(
163 161 version=version_re, capabilities=capabilities_re,
164 162 url=url_re, ra_client=ra_client_re, client=client_re))
165 163 matcher = regex.match(buffer_)
164
166 165 return matcher.groupdict() if matcher else None
167 166
167 def _match_repo_name(self, url):
168 """
169 Given an server url, try to match it against ALL known repository names.
170 This handles a tricky SVN case for SSH and subdir commits.
171 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
172 result in the url with this subdir added.
173 """
174 # case 1 direct match, we don't do any "heavy" lookups
175 if url in self.server.user_permissions:
176 return url
177
178 log.debug('Extracting repository name from subdir path %s', url)
179 # case 2 we check all permissions, and match closes possible case...
180 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
181 # to assume that it will have the repo name as prefix, we ensure the prefix
182 # for similar repositories isn't matched by adding a /
183 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
184 for repo_name in self.server.user_permissions:
185 repo_name_prefix = repo_name + '/'
186 if url.startswith(repo_name_prefix):
187 log.debug('Found prefix %s match, returning proper repository name',
188 repo_name_prefix)
189 return repo_name
190
191 return
192
168 193 def run(self, extras):
169 194 action = 'pull'
170 195 self.create_svn_config()
171 196 self.start()
172 197
173 198 first_response = self.get_first_client_response()
174 199 if not first_response:
175 200 return self.fail("Repository name cannot be extracted")
176 201
177 202 url_parts = urlparse.urlparse(first_response['url'])
178 self.server.repo_name = url_parts.path.strip('/')
203
204 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
179 205
180 206 exit_code = self.server._check_permissions(action)
181 207 if exit_code:
182 208 return exit_code
183 209
184 210 # set the readonly flag to False if we have proper permissions
185 211 if self.server.has_write_perm():
186 212 self.read_only = False
187 213 self.server.update_environment(action=action, extras=extras)
188 214
189 215 self.patch_first_client_response(first_response)
190 216 self.sync()
191 217 return self.return_code
192 218
193 219
194 220 class SubversionServer(VcsServer):
195 221 backend = 'svn'
196 222
197 223 def __init__(self, store, ini_path, repo_name,
198 224 user, user_permissions, config, env):
199 225 super(SubversionServer, self)\
200 226 .__init__(user, user_permissions, config, env)
201 227 self.store = store
202 228 self.ini_path = ini_path
203 # this is set in .run() from input stream
229 # NOTE(dan): repo_name at this point is empty,
230 # this is set later in .run() based from parsed input stream
204 231 self.repo_name = repo_name
205 self._path = self.svn_path = config.get(
206 'app:main', 'ssh.executable.svn')
232 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
207 233
208 234 self.tunnel = SubversionTunnelWrapper(server=self)
209 235
210 236 def _handle_tunnel(self, extras):
211 237
212 238 # pre-auth
213 239 action = 'pull'
214 240 # Special case for SVN, we extract repo name at later stage
215 241 # exit_code = self._check_permissions(action)
216 242 # if exit_code:
217 243 # return exit_code, False
218 244
219 245 req = self.env['request']
220 246 server_url = req.host_url + req.script_name
221 247 extras['server_url'] = server_url
222 248
223 249 log.debug('Using %s binaries from path %s', self.backend, self._path)
224 250 exit_code = self.tunnel.run(extras)
225 251
226 252 return exit_code, action == "push"
227 253
228 254
@@ -1,124 +1,204 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
25 25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
26 26
27 27
28 28 class SubversionServerCreator(object):
29 29 root = '/tmp/repo/path/'
30 30 svn_path = '/usr/local/bin/svnserve'
31 31 config_data = {
32 32 'app:main': {
33 33 'ssh.executable.svn': svn_path,
34 34 'vcs.hooks.protocol': 'http',
35 35 }
36 36 }
37 37 repo_name = 'test-svn'
38 38 user = plain_dummy_user()
39 39
40 40 def __init__(self):
41 41 def config_get(part, key):
42 42 return self.config_data.get(part, {}).get(key)
43 43 self.config_mock = mock.Mock()
44 44 self.config_mock.get = mock.Mock(side_effect=config_get)
45 45
46 46 def create(self, **kwargs):
47 47 parameters = {
48 48 'store': self.root,
49 49 'repo_name': self.repo_name,
50 50 'ini_path': '',
51 51 'user': self.user,
52 52 'user_permissions': {
53 53 self.repo_name: 'repository.admin'
54 54 },
55 55 'config': self.config_mock,
56 56 'env': plain_dummy_env()
57 57 }
58 58
59 59 parameters.update(kwargs)
60 60 server = SubversionServer(**parameters)
61 61 return server
62 62
63 63
64 64 @pytest.fixture()
65 65 def svn_server(app):
66 66 return SubversionServerCreator()
67 67
68 68
69 69 class TestSubversionServer(object):
70 70 def test_command(self, svn_server):
71 71 server = svn_server.create()
72 72 expected_command = [
73 73 svn_server.svn_path, '-t', '--config-file',
74 74 server.tunnel.svn_conf_path, '-r', svn_server.root
75 75 ]
76 76
77 77 assert expected_command == server.tunnel.command()
78 78
79 79 @pytest.mark.parametrize('permissions, action, code', [
80 80 ({}, 'pull', -2),
81 81 ({'test-svn': 'repository.read'}, 'pull', 0),
82 82 ({'test-svn': 'repository.read'}, 'push', -2),
83 83 ({'test-svn': 'repository.write'}, 'push', 0),
84 84 ({'test-svn': 'repository.admin'}, 'push', 0),
85 85
86 86 ])
87 87 def test_permission_checks(self, svn_server, permissions, action, code):
88 88 server = svn_server.create(user_permissions=permissions)
89 89 result = server._check_permissions(action)
90 90 assert result is code
91 91
92 @pytest.mark.parametrize('permissions, access_paths, expected_match', [
93 # not matched repository name
94 ({
95 'test-svn': ''
96 }, ['test-svn-1', 'test-svn-1/subpath'],
97 None),
98
99 # exact match
100 ({
101 'test-svn': ''
102 },
103 ['test-svn'],
104 'test-svn'),
105
106 # subdir commits
107 ({
108 'test-svn': ''
109 },
110 ['test-svn/foo',
111 'test-svn/foo/test-svn',
112 'test-svn/trunk/development.txt',
113 ],
114 'test-svn'),
115
116 # subgroups + similar patterns
117 ({
118 'test-svn': '',
119 'test-svn-1': '',
120 'test-svn-subgroup/test-svn': '',
121
122 },
123 ['test-svn-1',
124 'test-svn-1/foo/test-svn',
125 'test-svn-1/test-svn',
126 ],
127 'test-svn-1'),
128
129 # subgroups + similar patterns
130 ({
131 'test-svn-1': '',
132 'test-svn-10': '',
133 'test-svn-100': '',
134 },
135 ['test-svn-10',
136 'test-svn-10/foo/test-svn',
137 'test-svn-10/test-svn',
138 ],
139 'test-svn-10'),
140
141 # subgroups + similar patterns
142 ({
143 'name': '',
144 'nameContains': '',
145 'nameContainsThis': '',
146 },
147 ['nameContains',
148 'nameContains/This',
149 'nameContains/This/test-svn',
150 ],
151 'nameContains'),
152
153 # subgroups + similar patterns
154 ({
155 'test-svn': '',
156 'test-svn-1': '',
157 'test-svn-subgroup/test-svn': '',
158
159 },
160 ['test-svn-subgroup/test-svn',
161 'test-svn-subgroup/test-svn/foo/test-svn',
162 'test-svn-subgroup/test-svn/trunk/example.txt',
163 ],
164 'test-svn-subgroup/test-svn'),
165 ])
166 def test_repo_extraction_on_subdir(self, svn_server, permissions, access_paths, expected_match):
167 server = svn_server.create(user_permissions=permissions)
168 for path in access_paths:
169 repo_name = server.tunnel._match_repo_name(path)
170 assert repo_name == expected_match
171
92 172 def test_run_returns_executes_command(self, svn_server):
93 173 server = svn_server.create()
94 174 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
95 175 with mock.patch.object(
96 176 SubversionTunnelWrapper, 'get_first_client_response',
97 177 return_value={'url': 'http://server/test-svn'}):
98 178 with mock.patch.object(
99 179 SubversionTunnelWrapper, 'patch_first_client_response',
100 180 return_value=0):
101 181 with mock.patch.object(
102 182 SubversionTunnelWrapper, 'sync',
103 183 return_value=0):
104 184 with mock.patch.object(
105 185 SubversionTunnelWrapper, 'command',
106 186 return_value=['date']):
107 187
108 188 exit_code = server.run()
109 189 # SVN has this differently configured, and we get in our mock env
110 190 # None as return code
111 191 assert exit_code == (None, False)
112 192
113 193 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
114 194 server = svn_server.create()
115 195 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
116 196 with mock.patch.object(
117 197 SubversionTunnelWrapper, 'command',
118 198 return_value=['date']):
119 199 with mock.patch.object(
120 200 SubversionTunnelWrapper, 'get_first_client_response',
121 201 return_value=None):
122 202 exit_code = server.run()
123 203
124 204 assert exit_code == (1, False)
General Comments 0
You need to be logged in to leave comments. Login now