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