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