##// END OF EJS Templates
ssh-support: enabled full handling of all backends via SSH....
marcink -
r2187:47a0c0ed default
parent child Browse files
Show More
@@ -0,0 +1,202 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import re
23 import logging
24 import datetime
25 import ConfigParser
26
27 from rhodecode.model.db import Session, User, UserSshKeys
28 from rhodecode.model.scm import ScmModel
29
30 from .hg import MercurialServer
31 from .git import GitServer
32 from .svn import SubversionServer
33 log = logging.getLogger(__name__)
34
35
36 class SshWrapper(object):
37
38 def __init__(self, command, connection_info, mode,
39 user, user_id, key_id, shell, ini_path, env):
40 self.command = command
41 self.connection_info = connection_info
42 self.mode = mode
43 self.user = user
44 self.user_id = user_id
45 self.key_id = key_id
46 self.shell = shell
47 self.ini_path = ini_path
48 self.env = env
49
50 self.config = self.parse_config(ini_path)
51 self.server_impl = None
52
53 def parse_config(self, config_path):
54 parser = ConfigParser.ConfigParser()
55 parser.read(config_path)
56 return parser
57
58 def update_key_access_time(self, key_id):
59 key = UserSshKeys().query().filter(
60 UserSshKeys.ssh_key_id == key_id).scalar()
61 if key:
62 key.accessed_on = datetime.datetime.utcnow()
63 Session().add(key)
64 Session().commit()
65 log.debug('Update key `%s` access time', key_id)
66
67 def get_connection_info(self):
68 """
69 connection_info
70
71 Identifies the client and server ends of the connection.
72 The variable contains four space-separated values: client IP address,
73 client port number, server IP address, and server port number.
74 """
75 conn = dict(
76 client_ip=None,
77 client_port=None,
78 server_ip=None,
79 server_port=None,
80 )
81
82 info = self.connection_info.split(' ')
83 if len(info) == 4:
84 conn['client_ip'] = info[0]
85 conn['client_port'] = info[1]
86 conn['server_ip'] = info[2]
87 conn['server_port'] = info[3]
88
89 return conn
90
91 def get_repo_details(self, mode):
92 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
93 mode = mode
94 repo_name = None
95
96 hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$'
97 hg_match = re.match(hg_pattern, self.command)
98 if hg_match is not None:
99 vcs_type = 'hg'
100 repo_name = hg_match.group(1).strip('/')
101 return vcs_type, repo_name, mode
102
103 git_pattern = (
104 r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
105 git_match = re.match(git_pattern, self.command)
106 if git_match is not None:
107 vcs_type = 'git'
108 repo_name = git_match.group(2).strip('/')
109 mode = git_match.group(1)
110 return vcs_type, repo_name, mode
111
112 svn_pattern = r'^svnserve -t'
113 svn_match = re.match(svn_pattern, self.command)
114
115 if svn_match is not None:
116 vcs_type = 'svn'
117 # Repo name should be extracted from the input stream
118 return vcs_type, repo_name, mode
119
120 return vcs_type, repo_name, mode
121
122 def serve(self, vcs, repo, mode, user, permissions):
123 store = ScmModel().repos_path
124
125 log.debug(
126 'VCS detected:`%s` mode: `%s` repo_name: %s', vcs, mode, repo)
127
128 if vcs == 'hg':
129 server = MercurialServer(
130 store=store, ini_path=self.ini_path,
131 repo_name=repo, user=user,
132 user_permissions=permissions, config=self.config, env=self.env)
133 self.server_impl = server
134 return server.run()
135
136 elif vcs == 'git':
137 server = GitServer(
138 store=store, ini_path=self.ini_path,
139 repo_name=repo, repo_mode=mode, user=user,
140 user_permissions=permissions, config=self.config, env=self.env)
141 self.server_impl = server
142 return server.run()
143
144 elif vcs == 'svn':
145 server = SubversionServer(
146 store=store, ini_path=self.ini_path,
147 repo_name=None, user=user,
148 user_permissions=permissions, config=self.config, env=self.env)
149 self.server_impl = server
150 return server.run()
151
152 else:
153 raise Exception('Unrecognised VCS: {}'.format(vcs))
154
155 def wrap(self):
156 mode = self.mode
157 user = self.user
158 user_id = self.user_id
159 key_id = self.key_id
160 shell = self.shell
161
162 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
163
164 log.debug(
165 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` '
166 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
167 mode, user, user_id, shell, self.command,
168 scm_detected, scm_mode, scm_repo)
169
170 # update last access time for this key
171 self.update_key_access_time(key_id)
172
173 log.debug('SSH Connection info %s', self.get_connection_info())
174
175 if shell and self.command is None:
176 log.info(
177 'Dropping to shell, no command given and shell is allowed')
178 os.execl('/bin/bash', '-l')
179 exit_code = 1
180
181 elif scm_detected:
182 user = User.get(user_id)
183 auth_user = user.AuthUser()
184 permissions = auth_user.permissions['repositories']
185
186 try:
187 exit_code, is_updated = self.serve(
188 scm_detected, scm_repo, scm_mode, user, permissions)
189 except Exception:
190 log.exception('Error occurred during execution of SshWrapper')
191 exit_code = -1
192
193 elif self.command is None and shell is False:
194 log.error('No Command given.')
195 exit_code = -1
196
197 else:
198 log.error(
199 'Unhandled Command: "%s" Aborting.', self.command)
200 exit_code = -1
201
202 return exit_code
@@ -0,0 +1,149 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import sys
23 import json
24 import logging
25
26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
27 from rhodecode.lib import hooks_utils
28 from rhodecode.model.scm import ScmModel
29
30 log = logging.getLogger(__name__)
31
32
33 class VcsServer(object):
34 _path = None # set executable path for hg/git/svn binary
35 backend = None # set in child classes
36 tunnel = None # subprocess handling tunnel
37 write_perms = ['repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39
40 def __init__(self, user, user_permissions, config, env):
41 self.user = user
42 self.user_permissions = user_permissions
43 self.config = config
44 self.env = env
45 self.stdin = sys.stdin
46
47 self.repo_name = None
48 self.repo_mode = None
49 self.store = ''
50 self.ini_path = ''
51
52 def _invalidate_cache(self, repo_name):
53 """
54 Set's cache for this repository for invalidation on next access
55
56 :param repo_name: full repo name, also a cache key
57 """
58 ScmModel().mark_for_invalidation(repo_name)
59
60 def has_write_perm(self):
61 permission = self.user_permissions.get(self.repo_name)
62 if permission in ['repository.write', 'repository.admin']:
63 return True
64
65 return False
66
67 def _check_permissions(self, action):
68 permission = self.user_permissions.get(self.repo_name)
69 log.debug(
70 'permission for %s on %s are: %s',
71 self.user, self.repo_name, permission)
72
73 if action == 'pull':
74 if permission in self.read_perms:
75 log.info(
76 'READ Permissions for User "%s" detected to repo "%s"!',
77 self.user, self.repo_name)
78 return 0
79 else:
80 if permission in self.write_perms:
81 log.info(
82 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
83 self.user, self.repo_name)
84 return 0
85
86 log.error('Cannot properly fetch or allow user permissions. '
87 'Return value is: %s, req action: %s', permission, action)
88 return -2
89
90 def update_environment(self, action, extras=None):
91
92 scm_data = {
93 'ip': os.environ['SSH_CLIENT'].split()[0],
94 'username': self.user.username,
95 'action': action,
96 'repository': self.repo_name,
97 'scm': self.backend,
98 'config': self.ini_path,
99 'make_lock': None,
100 'locked_by': [None, None],
101 'server_url': None,
102 'is_shadow_repo': False,
103 'hooks_module': 'rhodecode.lib.hooks_daemon',
104 'hooks': ['push', 'pull'],
105 'SSH': True,
106 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name)
107 }
108 if extras:
109 scm_data.update(extras)
110 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
111
112 def get_root_store(self):
113 root_store = self.store
114 if not root_store.endswith('/'):
115 # always append trailing slash
116 root_store = root_store + '/'
117 return root_store
118
119 def _handle_tunnel(self, extras):
120 # pre-auth
121 action = 'pull'
122 exit_code = self._check_permissions(action)
123 if exit_code:
124 return exit_code, False
125
126 req = self.env['request']
127 server_url = req.host_url + req.script_name
128 extras['server_url'] = server_url
129
130 log.debug('Using %s binaries from path %s', self.backend, self._path)
131 exit_code = self.tunnel.run(extras)
132
133 return exit_code, action == "push"
134
135 def run(self):
136 extras = {}
137 HOOKS_PROTOCOL = self.config.get('app:main', 'vcs.hooks.protocol')
138
139 callback_daemon, extras = prepare_callback_daemon(
140 extras, protocol=HOOKS_PROTOCOL,
141 use_direct_calls=False)
142
143 with callback_daemon:
144 try:
145 return self._handle_tunnel(extras)
146 finally:
147 log.debug('Running cleanup with cache invalidation')
148 if self.repo_name:
149 self._invalidate_cache(self.repo_name)
@@ -0,0 +1,75 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import sys
23 import logging
24
25 from .base import VcsServer
26
27 log = logging.getLogger(__name__)
28
29
30 class GitTunnelWrapper(object):
31 process = None
32
33 def __init__(self, server):
34 self.server = server
35 self.stdin = sys.stdin
36 self.stdout = sys.stdout
37
38 def create_hooks_env(self):
39 pass
40
41 def command(self):
42 root = self.server.get_root_store()
43 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
44 root=root, git_path=self.server.git_path,
45 mode=self.server.repo_mode, repo_name=self.server.repo_name)
46 log.debug("Final CMD: %s", command)
47 return command
48
49 def run(self, extras):
50 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
51 exit_code = self.server._check_permissions(action)
52 if exit_code:
53 return exit_code
54
55 self.server.update_environment(action=action, extras=extras)
56 self.create_hooks_env()
57 return os.system(self.command())
58
59
60 class GitServer(VcsServer):
61 backend = 'git'
62
63 def __init__(self, store, ini_path, repo_name, repo_mode,
64 user, user_permissions, config, env):
65 super(GitServer, self).\
66 __init__(user, user_permissions, config, env)
67
68 self.store = store
69 self.ini_path = ini_path
70 self.repo_name = repo_name
71 self._path = self.git_path = config.get(
72 'app:main', 'ssh.executable.git')
73
74 self.repo_mode = repo_mode
75 self.tunnel = GitTunnelWrapper(server=self)
@@ -0,0 +1,123 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import sys
23 import shutil
24 import logging
25 import tempfile
26 import textwrap
27
28 from .base import VcsServer
29
30 log = logging.getLogger(__name__)
31
32
33 class MercurialTunnelWrapper(object):
34 process = None
35
36 def __init__(self, server):
37 self.server = server
38 self.stdin = sys.stdin
39 self.stdout = sys.stdout
40 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
41 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
42
43 def create_hooks_env(self):
44
45 content = textwrap.dedent(
46 '''
47 # SSH hooks version=1.0.0
48 [hooks]
49 pretxnchangegroup.ssh_auth=python:vcsserver.hooks.pre_push_ssh_auth
50 pretxnchangegroup.ssh=python:vcsserver.hooks.pre_push_ssh
51 changegroup.ssh=python:vcsserver.hooks.post_push_ssh
52
53 preoutgoing.ssh=python:vcsserver.hooks.pre_pull_ssh
54 outgoing.ssh=python:vcsserver.hooks.post_pull_ssh
55
56 '''
57 )
58
59 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
60 hooks_env_file.write(content)
61 root = self.server.get_root_store()
62
63 hgrc_custom = os.path.join(
64 root, self.server.repo_name, '.hg', 'hgrc_rhodecode')
65 log.debug('Wrote custom hgrc file under %s', hgrc_custom)
66 shutil.move(
67 self.hooks_env_path, hgrc_custom)
68
69 hgrc_main = os.path.join(
70 root, self.server.repo_name, '.hg', 'hgrc')
71 include_marker = '%include hgrc_rhodecode'
72
73 if not os.path.isfile(hgrc_main):
74 os.mknod(hgrc_main)
75
76 with open(hgrc_main, 'rb') as f:
77 data = f.read()
78 has_marker = include_marker in data
79
80 if not has_marker:
81 log.debug('Adding include marker for hooks')
82 with open(hgrc_main, 'wa') as f:
83 f.write(textwrap.dedent('''
84 # added by RhodeCode
85 {}
86 '''.format(include_marker)))
87
88 def command(self):
89 root = self.server.get_root_store()
90
91 command = (
92 "cd {root}; {hg_path} -R {root}{repo_name} "
93 "serve --stdio".format(
94 root=root, hg_path=self.server.hg_path,
95 repo_name=self.server.repo_name))
96 log.debug("Final CMD: %s", command)
97 return command
98
99 def run(self, extras):
100 # at this point we cannot tell, we do further ACL checks
101 # inside the hooks
102 action = '?'
103 # permissions are check via `pre_push_ssh_auth` hook
104 self.server.update_environment(action=action, extras=extras)
105 self.create_hooks_env()
106 return os.system(self.command())
107
108
109 class MercurialServer(VcsServer):
110 backend = 'hg'
111
112 def __init__(self, store, ini_path, repo_name,
113 user, user_permissions, config, env):
114 super(MercurialServer, self).\
115 __init__(user, user_permissions, config, env)
116
117 self.store = store
118 self.ini_path = ini_path
119 self.repo_name = repo_name
120 self._path = self.hg_path = config.get(
121 'app:main', 'ssh.executable.hg')
122
123 self.tunnel = MercurialTunnelWrapper(server=self)
@@ -0,0 +1,228 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import re
23 import sys
24 import logging
25 import signal
26 import tempfile
27 from subprocess import Popen, PIPE
28 import urlparse
29
30 from .base import VcsServer
31
32 log = logging.getLogger(__name__)
33
34
35 class SubversionTunnelWrapper(object):
36 process = None
37
38 def __init__(self, server):
39 self.server = server
40 self.timeout = 30
41 self.stdin = sys.stdin
42 self.stdout = sys.stdout
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45
46 self.read_only = True # flag that we set to make the hooks readonly
47
48 def create_svn_config(self):
49 content = (
50 '[general]\n'
51 'hooks-env = {}\n').format(self.hooks_env_path)
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 config_file.write(content)
54
55 def create_hooks_env(self):
56 content = (
57 '[default]\n'
58 'LANG = en_US.UTF-8\n')
59 if self.read_only:
60 content += 'SSH_READ_ONLY = 1\n'
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 hooks_env_file.write(content)
63
64 def remove_configs(self):
65 os.remove(self.svn_conf_path)
66 os.remove(self.hooks_env_path)
67
68 def command(self):
69 root = self.server.get_root_store()
70 command = [
71 self.server.svn_path, '-t',
72 '--config-file', self.svn_conf_path,
73 '-r', root]
74 log.debug("Final CMD: %s", command)
75 return command
76
77 def start(self):
78 command = self.command()
79 self.process = Popen(command, stdin=PIPE)
80
81 def sync(self):
82 while self.process.poll() is None:
83 next_byte = self.stdin.read(1)
84 if not next_byte:
85 break
86 self.process.stdin.write(next_byte)
87 self.remove_configs()
88
89 @property
90 def return_code(self):
91 return self.process.returncode
92
93 def get_first_client_response(self):
94 signal.signal(signal.SIGALRM, self.interrupt)
95 signal.alarm(self.timeout)
96 first_response = self._read_first_client_response()
97 signal.alarm(0)
98 return (
99 self._parse_first_client_response(first_response)
100 if first_response else None)
101
102 def patch_first_client_response(self, response, **kwargs):
103 self.create_hooks_env()
104 data = response.copy()
105 data.update(kwargs)
106 data['url'] = self._svn_string(data['url'])
107 data['ra_client'] = self._svn_string(data['ra_client'])
108 data['client'] = data['client'] or ''
109 buffer_ = (
110 "( {version} ( {capabilities} ) {url}{ra_client}"
111 "( {client}) ) ".format(**data))
112 self.process.stdin.write(buffer_)
113
114 def fail(self, message):
115 print(
116 "( failure ( ( 210005 {message} 0: 0 ) ) )".format(
117 message=self._svn_string(message)))
118 self.remove_configs()
119 self.process.kill()
120
121 def interrupt(self, signum, frame):
122 self.fail("Exited by timeout")
123
124 def _svn_string(self, str_):
125 if not str_:
126 return ''
127 return '{length}:{string} '.format(length=len(str_), string=str_)
128
129 def _read_first_client_response(self):
130 buffer_ = ""
131 brackets_stack = []
132 while True:
133 next_byte = self.stdin.read(1)
134 buffer_ += next_byte
135 if next_byte == "(":
136 brackets_stack.append(next_byte)
137 elif next_byte == ")":
138 brackets_stack.pop()
139 elif next_byte == " " and not brackets_stack:
140 break
141 return buffer_
142
143 def _parse_first_client_response(self, buffer_):
144 """
145 According to the Subversion RA protocol, the first request
146 should look like:
147
148 ( version:number ( cap:word ... ) url:string ? ra-client:string
149 ( ? client:string ) )
150
151 Please check https://svn.apache.org/repos/asf/subversion/trunk/
152 subversion/libsvn_ra_svn/protocol
153 """
154 version_re = r'(?P<version>\d+)'
155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
156 url_re = r'\d+\:(?P<url>[\W\w]+)'
157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
159 regex = re.compile(
160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
161 r'\(\s{client}\)\s\)\s*$'.format(
162 version=version_re, capabilities=capabilities_re,
163 url=url_re, ra_client=ra_client_re, client=client_re))
164 matcher = regex.match(buffer_)
165 return matcher.groupdict() if matcher else None
166
167 def run(self, extras):
168 action = 'pull'
169 self.create_svn_config()
170 self.start()
171
172 first_response = self.get_first_client_response()
173 if not first_response:
174 self.fail("Repository name cannot be extracted")
175 return 1
176
177 url_parts = urlparse.urlparse(first_response['url'])
178 self.server.repo_name = url_parts.path.strip('/')
179
180 exit_code = self.server._check_permissions(action)
181 if exit_code:
182 return exit_code
183
184 # set the readonly flag to False if we have proper permissions
185 if self.server.has_write_perm():
186 self.read_only = False
187 self.server.update_environment(action=action, extras=extras)
188
189 self.patch_first_client_response(first_response)
190 self.sync()
191 return self.return_code
192
193
194 class SubversionServer(VcsServer):
195 backend = 'svn'
196
197 def __init__(self, store, ini_path, repo_name,
198 user, user_permissions, config, env):
199 super(SubversionServer, self)\
200 .__init__(user, user_permissions, config, env)
201 self.store = store
202 self.ini_path = ini_path
203 # this is set in .run() from input stream
204 self.repo_name = repo_name
205 self._path = self.svn_path = config.get(
206 'app:main', 'ssh.executable.svn')
207
208 self.tunnel = SubversionTunnelWrapper(server=self)
209
210 def _handle_tunnel(self, extras):
211
212 # pre-auth
213 action = 'pull'
214 # Special case for SVN, we extract repo name at later stage
215 # exit_code = self._check_permissions(action)
216 # if exit_code:
217 # return exit_code, False
218
219 req = self.env['request']
220 server_url = req.host_url + req.script_name
221 extras['server_url'] = server_url
222
223 log.debug('Using %s binaries from path %s', self.backend, self._path)
224 exit_code = self.tunnel.run(extras)
225
226 return exit_code, action == "push"
227
228
@@ -0,0 +1,62 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import pytest
23 import ConfigParser
24
25 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper
26 from rhodecode.lib.utils2 import AttributeDict
27
28
29 @pytest.fixture
30 def dummy_conf_file(tmpdir):
31 conf = ConfigParser.ConfigParser()
32 conf.add_section('app:main')
33 conf.set('app:main', 'ssh.executable.hg', '/usr/bin/hg')
34 conf.set('app:main', 'ssh.executable.git', '/usr/bin/git')
35 conf.set('app:main', 'ssh.executable.svn', '/usr/bin/svnserve')
36
37 f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini')
38 with open(f_path, 'wb') as f:
39 conf.write(f)
40
41 return os.path.join(f_path)
42
43
44 @pytest.fixture
45 def dummy_env():
46 return {
47 'request':
48 AttributeDict(host_url='http://localhost', script_name='/')
49 }
50
51
52 @pytest.fixture
53 def dummy_user():
54 return AttributeDict(username='test_user')
55
56
57 @pytest.fixture
58 def ssh_wrapper(app, dummy_conf_file, dummy_env):
59 conn_info = '127.0.0.1 22 10.0.0.1 443'
60 return SshWrapper(
61 'random command', conn_info, 'auto', 'admin', '1', key_id='1',
62 shell=False, ini_path=dummy_conf_file, env=dummy_env)
@@ -1,81 +1,83 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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 import click
26 26
27 27 from pyramid.paster import bootstrap, setup_logging
28 28 from pyramid.request import Request
29 29
30 30 from .backends import SshWrapper
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 def setup_custom_logging(ini_path, debug):
36 36 if debug:
37 37 # enabled rhodecode.ini controlled logging setup
38 38 setup_logging(ini_path)
39 39 else:
40 40 # configure logging in a mode that doesn't print anything.
41 41 # in case of regularly configured logging it gets printed out back
42 42 # to the client doing an SSH command.
43 43 logger = logging.getLogger('')
44 44 null = logging.NullHandler()
45 45 # add the handler to the root logger
46 46 logger.handlers = [null]
47 47
48 48
49 49 @click.command()
50 50 @click.argument('ini_path', type=click.Path(exists=True))
51 51 @click.option(
52 52 '--mode', '-m', required=False, default='auto',
53 53 type=click.Choice(['auto', 'vcs', 'git', 'hg', 'svn', 'test']),
54 54 help='mode of operation')
55 55 @click.option('--user', help='Username for which the command will be executed')
56 56 @click.option('--user-id', help='User ID for which the command will be executed')
57 57 @click.option('--key-id', help='ID of the key from the database')
58 58 @click.option('--shell', '-s', is_flag=True, help='Allow Shell')
59 59 @click.option('--debug', is_flag=True, help='Enabled detailed output logging')
60 60 def main(ini_path, mode, user, user_id, key_id, shell, debug):
61 61 setup_custom_logging(ini_path, debug)
62 62
63 63 command = os.environ.get('SSH_ORIGINAL_COMMAND', '')
64 64 if not command and mode not in ['test']:
65 65 raise ValueError(
66 66 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.'
67 67 'Please make sure this is set and available during execution '
68 68 'of this script.')
69 69 connection_info = os.environ.get('SSH_CONNECTION', '')
70 request = Request.blank('/', base_url='http://rhodecode-ssh-wrapper/')
70
71 # TODO(marcink): configure the running host...
72 request = Request.blank('/', base_url='http://localhost:8080')
71 73 with bootstrap(ini_path, request=request) as env:
72 74 try:
73 75 ssh_wrapper = SshWrapper(
74 76 command, connection_info, mode,
75 user, user_id, key_id, shell, ini_path)
77 user, user_id, key_id, shell, ini_path, env)
76 78 except Exception:
77 79 log.exception('Failed to execute SshWrapper')
78 80 sys.exit(-5)
79 81
80 82 return_code = ssh_wrapper.wrap()
81 83 sys.exit(return_code)
@@ -1,196 +1,145 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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 json
22
22 import mock
23 23 import pytest
24 from mock import Mock, patch
25 24
26 25 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
27
28
29 @pytest.fixture
30 def git_server():
31 return GitServerCreator()
26 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
32 27
33 28
34 29 class GitServerCreator(object):
35 30 root = '/tmp/repo/path/'
36 git_path = '/usr/local/bin/'
31 git_path = '/usr/local/bin/git'
37 32 config_data = {
38 33 'app:main': {
39 'ssh.executable.git': git_path
34 'ssh.executable.git': git_path,
35 'vcs.hooks.protocol': 'http',
40 36 }
41 37 }
42 38 repo_name = 'test_git'
43 39 repo_mode = 'receive-pack'
44 user = 'vcs'
40 user = dummy_user()
45 41
46 42 def __init__(self):
47 43 def config_get(part, key):
48 44 return self.config_data.get(part, {}).get(key)
49 self.config_mock = Mock()
50 self.config_mock.get = Mock(side_effect=config_get)
45 self.config_mock = mock.Mock()
46 self.config_mock.get = mock.Mock(side_effect=config_get)
51 47
52 48 def create(self, **kwargs):
53 49 parameters = {
54 'store': {'path': self.root},
50 'store': self.root,
55 51 'ini_path': '',
56 52 'user': self.user,
57 53 'repo_name': self.repo_name,
58 54 'repo_mode': self.repo_mode,
59 55 'user_permissions': {
60 self.repo_name: 'repo_admin'
56 self.repo_name: 'repository.admin'
61 57 },
62 58 'config': self.config_mock,
59 'env': dummy_env()
63 60 }
64 61 parameters.update(kwargs)
65 62 server = GitServer(**parameters)
66 63 return server
67 64
68 65
66 @pytest.fixture
67 def git_server(app):
68 return GitServerCreator()
69
70
69 71 class TestGitServer(object):
72
70 73 def test_command(self, git_server):
71 74 server = git_server.create()
72 server.read_only = False
73 75 expected_command = (
74 'cd {root}; {git_path}-{repo_mode}'
75 ' \'{root}{repo_name}\''.format(
76 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
76 77 root=git_server.root, git_path=git_server.git_path,
77 78 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
78 79 )
79 assert expected_command == server.command
80 assert expected_command == server.tunnel.command()
81
82 @pytest.mark.parametrize('permissions, action, code', [
83 ({}, 'pull', -2),
84 ({'test_git': 'repository.read'}, 'pull', 0),
85 ({'test_git': 'repository.read'}, 'push', -2),
86 ({'test_git': 'repository.write'}, 'push', 0),
87 ({'test_git': 'repository.admin'}, 'push', 0),
80 88
81 def test_run_returns_exit_code_2_when_no_permissions(self, git_server, caplog):
89 ])
90 def test_permission_checks(self, git_server, permissions, action, code):
91 server = git_server.create(user_permissions=permissions)
92 result = server._check_permissions(action)
93 assert result is code
94
95 @pytest.mark.parametrize('permissions, value', [
96 ({}, False),
97 ({'test_git': 'repository.read'}, False),
98 ({'test_git': 'repository.write'}, True),
99 ({'test_git': 'repository.admin'}, True),
100
101 ])
102 def test_has_write_permissions(self, git_server, permissions, value):
103 server = git_server.create(user_permissions=permissions)
104 result = server.has_write_perm()
105 assert result is value
106
107 def test_run_returns_executes_command(self, git_server):
82 108 server = git_server.create()
83 with patch.object(server, '_check_permissions') as permissions_mock:
84 with patch.object(server, '_update_environment'):
85 permissions_mock.return_value = 2
109 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
110 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
111 _patch.return_value = 0
112 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
86 113 exit_code = server.run()
87 114
88 assert exit_code == (2, False)
89
90 def test_run_returns_executes_command(self, git_server, caplog):
91 server = git_server.create()
92 with patch.object(server, '_check_permissions') as permissions_mock:
93 with patch('os.system') as system_mock:
94 with patch.object(server, '_update_environment') as (
95 update_mock):
96 permissions_mock.return_value = 0
97 system_mock.return_value = 0
98 exit_code = server.run()
99
100 system_mock.assert_called_once_with(server.command)
101 update_mock.assert_called_once_with()
102
103 assert exit_code == (0, True)
115 assert exit_code == (0, False)
104 116
105 117 @pytest.mark.parametrize(
106 118 'repo_mode, action', [
107 119 ['receive-pack', 'push'],
108 120 ['upload-pack', 'pull']
109 121 ])
110 122 def test_update_environment(self, git_server, repo_mode, action):
111 123 server = git_server.create(repo_mode=repo_mode)
112 with patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
113 with patch('os.putenv') as putenv_mock:
114 server._update_environment()
124 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
125 with mock.patch('os.putenv') as putenv_mock:
126 server.update_environment(action)
115 127
116 128 expected_data = {
117 "username": git_server.user,
118 "scm": "git",
119 "repository": git_server.repo_name,
120 "make_lock": None,
121 "action": [action],
122 "ip": "10.10.10.10",
123 "locked_by": [None, None],
124 "config": ""
129 'username': git_server.user.username,
130 'scm': 'git',
131 'repository': git_server.repo_name,
132 'make_lock': None,
133 'action': action,
134 'ip': '10.10.10.10',
135 'locked_by': [None, None],
136 'config': '',
137 'server_url': None,
138 'hooks': ['push', 'pull'],
139 'is_shadow_repo': False,
140 'hooks_module': 'rhodecode.lib.hooks_daemon',
141 'SSH': True,
142 'SSH_PERMISSIONS': 'repository.admin',
125 143 }
126 144 args, kwargs = putenv_mock.call_args
127 145 assert json.loads(args[1]) == expected_data
128
129
130 class TestGitServerCheckPermissions(object):
131 def test_returns_2_when_no_permissions_found(self, git_server, caplog):
132 user_permissions = {}
133 server = git_server.create(user_permissions=user_permissions)
134 result = server._check_permissions()
135 assert result == 2
136
137 log_msg = 'permission for vcs on test_git are: None'
138 assert log_msg in [t[2] for t in caplog.record_tuples]
139
140 def test_returns_2_when_no_permissions(self, git_server, caplog):
141 user_permissions = {git_server.repo_name: 'repository.none'}
142 server = git_server.create(user_permissions=user_permissions)
143 result = server._check_permissions()
144 assert result == 2
145
146 log_msg = 'repo not found or no permissions'
147 assert log_msg in [t[2] for t in caplog.record_tuples]
148
149 @pytest.mark.parametrize(
150 'permission', ['repository.admin', 'repository.write'])
151 def test_access_allowed_when_user_has_write_permissions(
152 self, git_server, permission, caplog):
153 user_permissions = {git_server.repo_name: permission}
154 server = git_server.create(user_permissions=user_permissions)
155 result = server._check_permissions()
156 assert result is None
157
158 log_msg = 'Write Permissions for User "%s" granted to repo "%s"!' % (
159 git_server.user, git_server.repo_name)
160 assert log_msg in [t[2] for t in caplog.record_tuples]
161
162 def test_write_access_is_not_allowed_when_user_has_read_permission(
163 self, git_server, caplog):
164 user_permissions = {git_server.repo_name: 'repository.read'}
165 server = git_server.create(
166 user_permissions=user_permissions, repo_mode='receive-pack')
167 result = server._check_permissions()
168 assert result == -3
169
170 log_msg = 'Only Read Only access for User "%s" granted to repo "%s"! Failing!' % (
171 git_server.user, git_server.repo_name)
172 assert log_msg in [t[2] for t in caplog.record_tuples]
173
174 def test_read_access_allowed_when_user_has_read_permission(
175 self, git_server, caplog):
176 user_permissions = {git_server.repo_name: 'repository.read'}
177 server = git_server.create(
178 user_permissions=user_permissions, repo_mode='upload-pack')
179 result = server._check_permissions()
180 assert result is None
181
182 log_msg = 'Only Read Only access for User "%s" granted to repo "%s"!' % (
183 git_server.user, git_server.repo_name)
184 assert log_msg in [t[2] for t in caplog.record_tuples]
185
186 def test_returns_error_when_permission_not_recognised(
187 self, git_server, caplog):
188 user_permissions = {git_server.repo_name: 'repository.whatever'}
189 server = git_server.create(
190 user_permissions=user_permissions, repo_mode='upload-pack')
191 result = server._check_permissions()
192 assert result == -2
193
194 log_msg = 'Cannot properly fetch user permission. ' \
195 'Return value is: repository.whatever'
196 assert log_msg in [t[2] for t in caplog.record_tuples] No newline at end of file
@@ -1,146 +1,116 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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 import mock
21 22 import pytest
22 from mock import Mock, patch
23 23
24 24 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialServer
25
26
27 @pytest.fixture
28 def hg_server():
29 return MercurialServerCreator()
25 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
30 26
31 27
32 28 class MercurialServerCreator(object):
33 29 root = '/tmp/repo/path/'
34 30 hg_path = '/usr/local/bin/hg'
35 31
36 32 config_data = {
37 33 'app:main': {
38 'ssh.executable.hg': hg_path
34 'ssh.executable.hg': hg_path,
35 'vcs.hooks.protocol': 'http',
39 36 }
40 37 }
41 38 repo_name = 'test_hg'
42 user = 'vcs'
39 user = dummy_user()
43 40
44 41 def __init__(self):
45 42 def config_get(part, key):
46 43 return self.config_data.get(part, {}).get(key)
47 self.config_mock = Mock()
48 self.config_mock.get = Mock(side_effect=config_get)
44 self.config_mock = mock.Mock()
45 self.config_mock.get = mock.Mock(side_effect=config_get)
49 46
50 47 def create(self, **kwargs):
51 48 parameters = {
52 'store': {'path': self.root},
49 'store': self.root,
53 50 'ini_path': '',
54 51 'user': self.user,
55 52 'repo_name': self.repo_name,
56 53 'user_permissions': {
57 'test_hg': 'repo_admin'
54 'test_hg': 'repository.admin'
58 55 },
59 56 'config': self.config_mock,
57 'env': dummy_env()
60 58 }
61 59 parameters.update(kwargs)
62 60 server = MercurialServer(**parameters)
63 61 return server
64 62
65 63
64 @pytest.fixture
65 def hg_server(app):
66 return MercurialServerCreator()
67
68
66 69 class TestMercurialServer(object):
67 def test_read_only_command(self, hg_server):
70
71 def test_command(self, hg_server):
68 72 server = hg_server.create()
69 server.read_only = True
70 73 expected_command = (
71 'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio'
72 ' --config hooks.pretxnchangegroup="false"'.format(
73 root=hg_server.root, hg_path=hg_server.hg_path,
74 repo_name=hg_server.repo_name)
75 )
76 assert expected_command == server.command
77
78 def test_normal_command(self, hg_server):
79 server = hg_server.create()
80 server.read_only = False
81 expected_command = (
82 'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio '.format(
74 'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio'.format(
83 75 root=hg_server.root, hg_path=hg_server.hg_path,
84 76 repo_name=hg_server.repo_name)
85 77 )
86 assert expected_command == server.command
87
88 def test_access_rejected_when_permissions_are_not_found(self, hg_server, caplog):
89 user_permissions = {}
90 server = hg_server.create(user_permissions=user_permissions)
91 result = server._check_permissions()
92 assert result is False
93
94 log_msg = 'repo not found or no permissions'
95 assert log_msg in [t[2] for t in caplog.record_tuples]
78 assert expected_command == server.tunnel.command()
96 79
97 def test_access_rejected_when_no_permissions(self, hg_server, caplog):
98 user_permissions = {hg_server.repo_name: 'repository.none'}
99 server = hg_server.create(user_permissions=user_permissions)
100 result = server._check_permissions()
101 assert result is False
102
103 log_msg = 'repo not found or no permissions'
104 assert log_msg in [t[2] for t in caplog.record_tuples]
80 @pytest.mark.parametrize('permissions, action, code', [
81 ({}, 'pull', -2),
82 ({'test_hg': 'repository.read'}, 'pull', 0),
83 ({'test_hg': 'repository.read'}, 'push', -2),
84 ({'test_hg': 'repository.write'}, 'push', 0),
85 ({'test_hg': 'repository.admin'}, 'push', 0),
105 86
106 @pytest.mark.parametrize(
107 'permission', ['repository.admin', 'repository.write'])
108 def test_access_allowed_when_user_has_write_permissions(
109 self, hg_server, permission, caplog):
110 user_permissions = {hg_server.repo_name: permission}
111 server = hg_server.create(user_permissions=user_permissions)
112 result = server._check_permissions()
113 assert result is True
87 ])
88 def test_permission_checks(self, hg_server, permissions, action, code):
89 server = hg_server.create(user_permissions=permissions)
90 result = server._check_permissions(action)
91 assert result is code
114 92
115 assert server.read_only is False
116 log_msg = 'Write Permissions for User "vcs" granted to repo "test_hg"!'
117 assert log_msg in [t[2] for t in caplog.record_tuples]
118
119 def test_access_allowed_when_user_has_read_permissions(self, hg_server, caplog):
120 user_permissions = {hg_server.repo_name: 'repository.read'}
121 server = hg_server.create(user_permissions=user_permissions)
122 result = server._check_permissions()
123 assert result is True
93 @pytest.mark.parametrize('permissions, value', [
94 ({}, False),
95 ({'test_hg': 'repository.read'}, False),
96 ({'test_hg': 'repository.write'}, True),
97 ({'test_hg': 'repository.admin'}, True),
124 98
125 assert server.read_only is True
126 log_msg = 'Only Read Only access for User "%s" granted to repo "%s"!' % (
127 hg_server.user, hg_server.repo_name)
128 assert log_msg in [t[2] for t in caplog.record_tuples]
99 ])
100 def test_has_write_permissions(self, hg_server, permissions, value):
101 server = hg_server.create(user_permissions=permissions)
102 result = server.has_write_perm()
103 assert result is value
129 104
130 def test_run_returns_exit_code_2_when_no_permissions(self, hg_server, caplog):
105 def test_run_returns_executes_command(self, hg_server):
131 106 server = hg_server.create()
132 with patch.object(server, '_check_permissions') as permissions_mock:
133 permissions_mock.return_value = False
134 exit_code = server.run()
135 assert exit_code == (2, False)
136
137 def test_run_returns_executes_command(self, hg_server, caplog):
138 server = hg_server.create()
139 with patch.object(server, '_check_permissions') as permissions_mock:
140 with patch('os.system') as system_mock:
141 permissions_mock.return_value = True
142 system_mock.return_value = 0
107 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialTunnelWrapper
108 with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch:
109 _patch.return_value = 0
110 with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'):
143 111 exit_code = server.run()
144 112
145 system_mock.assert_called_once_with(server.command)
146 113 assert exit_code == (0, False)
114
115
116
@@ -1,136 +1,124 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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 import mock
21 22 import pytest
22 from mock import Mock, patch
23 23
24 24 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
25
26
27 @pytest.fixture
28 def svn_server():
29 return SubversionServerCreator()
25 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
30 26
31 27
32 28 class SubversionServerCreator(object):
33 29 root = '/tmp/repo/path/'
34 30 svn_path = '/usr/local/bin/svnserve'
35 31 config_data = {
36 32 'app:main': {
37 'ssh.executable.svn': svn_path
33 'ssh.executable.svn': svn_path,
34 'vcs.hooks.protocol': 'http',
38 35 }
39 36 }
40 37 repo_name = 'test-svn'
41 user = 'vcs'
38 user = dummy_user()
42 39
43 40 def __init__(self):
44 41 def config_get(part, key):
45 42 return self.config_data.get(part, {}).get(key)
46 self.config_mock = Mock()
47 self.config_mock.get = Mock(side_effect=config_get)
43 self.config_mock = mock.Mock()
44 self.config_mock.get = mock.Mock(side_effect=config_get)
48 45
49 46 def create(self, **kwargs):
50 47 parameters = {
51 'store': {'path': self.root},
48 'store': self.root,
49 'repo_name': self.repo_name,
52 50 'ini_path': '',
53 51 'user': self.user,
54 52 'user_permissions': {
55 self.repo_name: 'repo_admin'
53 self.repo_name: 'repository.admin'
56 54 },
57 55 'config': self.config_mock,
56 'env': dummy_env()
58 57 }
58
59 59 parameters.update(kwargs)
60 60 server = SubversionServer(**parameters)
61 61 return server
62 62
63 63
64 @pytest.fixture
65 def svn_server(app):
66 return SubversionServerCreator()
67
68
64 69 class TestSubversionServer(object):
65 def test_timeout_returns_value_from_config(self, svn_server):
70 def test_command(self, svn_server):
66 71 server = svn_server.create()
67 assert server.timeout == 30
72 expected_command = [
73 svn_server.svn_path, '-t', '--config-file',
74 server.tunnel.svn_conf_path, '-r', svn_server.root
75 ]
68 76
69 @pytest.mark.parametrize(
70 'permission', ['repository.admin', 'repository.write'])
71 def test_check_permissions_with_write_permissions(
72 self, svn_server, permission):
73 user_permissions = {svn_server.repo_name: permission}
74 server = svn_server.create(user_permissions=user_permissions)
75 server.tunnel = Mock()
76 server.repo_name = svn_server.repo_name
77 result = server._check_permissions()
78 assert result is True
79 assert server.tunnel.read_only is False
77 assert expected_command == server.tunnel.command()
80 78
81 def test_check_permissions_with_read_permissions(self, svn_server):
82 user_permissions = {svn_server.repo_name: 'repository.read'}
83 server = svn_server.create(user_permissions=user_permissions)
84 server.tunnel = Mock()
85 server.repo_name = svn_server.repo_name
86 result = server._check_permissions()
87 assert result is True
88 assert server.tunnel.read_only is True
79 @pytest.mark.parametrize('permissions, action, code', [
80 ({}, 'pull', -2),
81 ({'test-svn': 'repository.read'}, 'pull', 0),
82 ({'test-svn': 'repository.read'}, 'push', -2),
83 ({'test-svn': 'repository.write'}, 'push', 0),
84 ({'test-svn': 'repository.admin'}, 'push', 0),
85
86 ])
87 def test_permission_checks(self, svn_server, permissions, action, code):
88 server = svn_server.create(user_permissions=permissions)
89 result = server._check_permissions(action)
90 assert result is code
89 91
90 def test_check_permissions_with_no_permissions(self, svn_server, caplog):
91 tunnel_mock = Mock()
92 user_permissions = {}
93 server = svn_server.create(user_permissions=user_permissions)
94 server.tunnel = tunnel_mock
95 server.repo_name = svn_server.repo_name
96 result = server._check_permissions()
97 assert result is False
98 tunnel_mock.fail.assert_called_once_with(
99 "Not enough permissions for repository {}".format(
100 svn_server.repo_name))
101
102 def test_run_returns_1_when_repository_name_cannot_be_extracted(
103 self, svn_server):
104 server = svn_server.create()
105 with patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.SubversionTunnelWrapper') as tunnel_mock:
106 tunnel_mock().get_first_client_response.return_value = None
107 exit_code = server.run()
108 assert exit_code == (1, False)
109 tunnel_mock().fail.assert_called_once_with(
110 'Repository name cannot be extracted')
111
112 def test_run_returns_tunnel_return_code(self, svn_server, caplog):
92 def test_run_returns_executes_command(self, svn_server):
113 93 server = svn_server.create()
114 fake_response = {
115 'url': 'ssh+svn://test@example.com/test-svn/'
116 }
117 with patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.SubversionTunnelWrapper') as tunnel_mock:
118 with patch.object(server, '_check_permissions') as (
119 permissions_mock):
120 permissions_mock.return_value = True
121 tunnel = tunnel_mock()
122 tunnel.get_first_client_response.return_value = fake_response
123 tunnel.return_code = 0
124 exit_code = server.run()
125 permissions_mock.assert_called_once_with()
94 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
95 with mock.patch.object(
96 SubversionTunnelWrapper, 'get_first_client_response',
97 return_value={'url': 'http://server/test-svn'}):
98 with mock.patch.object(
99 SubversionTunnelWrapper, 'patch_first_client_response',
100 return_value=0):
101 with mock.patch.object(
102 SubversionTunnelWrapper, 'sync',
103 return_value=0):
104 with mock.patch.object(
105 SubversionTunnelWrapper, 'command',
106 return_value='date'):
126 107
127 expected_log_calls = sorted([
128 "Using subversion binaries from '%s'" % svn_server.svn_path
129 ])
108 exit_code = server.run()
109 # SVN has this differently configured, and we get in our mock env
110 # None as return code
111 assert exit_code == (None, False)
130 112
131 assert expected_log_calls == [t[2] for t in caplog.record_tuples]
113 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
114 server = svn_server.create()
115 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
116 with mock.patch.object(
117 SubversionTunnelWrapper, 'command',
118 return_value='date'):
119 with mock.patch.object(
120 SubversionTunnelWrapper, 'get_first_client_response',
121 return_value=None):
122 exit_code = server.run()
132 123
133 assert exit_code == (0, False)
134 tunnel.patch_first_client_response.assert_called_once_with(
135 fake_response)
136 tunnel.sync.assert_called_once_with()
124 assert exit_code == (1, False)
@@ -1,201 +1,54 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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 import os
21 import mock
20
22 21 import pytest
23 import ConfigParser
24
25 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper
26
27
28 @pytest.fixture
29 def dummy_conf(tmpdir):
30 conf = ConfigParser.ConfigParser()
31 conf.add_section('app:main')
32 conf.set('app:main', 'ssh.executable.hg', '/usr/bin/hg')
33 conf.set('app:main', 'ssh.executable.git', '/usr/bin/git')
34 conf.set('app:main', 'ssh.executable.svn', '/usr/bin/svnserve')
35
36 f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini')
37 with open(f_path, 'wb') as f:
38 conf.write(f)
39
40 return os.path.join(f_path)
41
42
43 class TestGetRepoDetails(object):
44 @pytest.mark.parametrize(
45 'command', [
46 'hg -R test-repo serve --stdio',
47 'hg -R test-repo serve --stdio'
48 ])
49 def test_hg_command_matched(self, command, dummy_conf):
50 wrapper = SshWrapper(command, 'auto', 'admin', '3', 'False', dummy_conf)
51 type_, name, mode = wrapper.get_repo_details('auto')
52 assert type_ == 'hg'
53 assert name == 'test-repo'
54 assert mode is 'auto'
55
56 @pytest.mark.parametrize(
57 'command', [
58 'hg test-repo serve --stdio',
59 'hg -R test-repo serve',
60 'hg serve --stdio',
61 'hg serve -R test-repo'
62 ])
63 def test_hg_command_not_matched(self, command, dummy_conf):
64 wrapper = SshWrapper(command, 'auto', 'admin', '3', 'False', dummy_conf)
65 type_, name, mode = wrapper.get_repo_details('auto')
66 assert type_ is None
67 assert name is None
68 assert mode is 'auto'
69
70
71 class TestServe(object):
72 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, dummy_conf):
73 with mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store'):
74 wrapper = SshWrapper('random command', 'auto', 'admin', '3', 'False', dummy_conf)
75
76 with pytest.raises(Exception) as exc_info:
77 wrapper.serve(
78 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
79 permissions={})
80 assert exc_info.value.message == 'Unrecognised VCS: microsoft-tfs'
81 22
82 23
83 class TestServeHg(object):
84
85 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
86 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
87 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
88 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.MercurialServer.run')
89 def test_serve_creates_hg_instance(
90 self, mercurial_run_mock, get_repo_store_mock, get_user_mock,
91 invalidate_cache_mock, dummy_conf):
92
93 repo_name = None
94 mercurial_run_mock.return_value = 0, True
95 get_user_mock.return_value = {repo_name: 'repository.admin'}
96 get_repo_store_mock.return_value = {'path': '/tmp'}
97
98 wrapper = SshWrapper('date', 'hg', 'admin', '3', 'False',
99 dummy_conf)
100 exit_code = wrapper.wrap()
101 assert exit_code == 0
102 assert mercurial_run_mock.called
103
104 assert get_repo_store_mock.called
105 assert get_user_mock.called
106 invalidate_cache_mock.assert_called_once_with(repo_name)
24 class TestSSHWrapper(object):
107 25
108 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
109 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
110 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
111 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.MercurialServer.run')
112 def test_serve_hg_invalidates_cache(
113 self, mercurial_run_mock, get_repo_store_mock, get_user_mock,
114 invalidate_cache_mock, dummy_conf):
115
116 repo_name = None
117 mercurial_run_mock.return_value = 0, True
118 get_user_mock.return_value = {repo_name: 'repository.admin'}
119 get_repo_store_mock.return_value = {'path': '/tmp'}
26 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, ssh_wrapper):
27 with pytest.raises(Exception) as exc_info:
28 ssh_wrapper.serve(
29 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
30 permissions={})
31 assert exc_info.value.message == 'Unrecognised VCS: microsoft-tfs'
120 32
121 wrapper = SshWrapper('date', 'hg', 'admin', '3', 'False',
122 dummy_conf)
123 exit_code = wrapper.wrap()
124 assert exit_code == 0
125 assert mercurial_run_mock.called
126
127 assert get_repo_store_mock.called
128 assert get_user_mock.called
129 invalidate_cache_mock.assert_called_once_with(repo_name)
130
131
132 class TestServeGit(object):
33 def test_parse_config(self, ssh_wrapper):
34 config = ssh_wrapper.parse_config(ssh_wrapper.ini_path)
35 assert config
133 36
134 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
135 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
136 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
137 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.GitServer.run')
138 def test_serve_creates_git_instance(self, git_run_mock, get_repo_store_mock, get_user_mock,
139 invalidate_cache_mock, dummy_conf):
140 repo_name = None
141 git_run_mock.return_value = 0, True
142 get_user_mock.return_value = {repo_name: 'repository.admin'}
143 get_repo_store_mock.return_value = {'path': '/tmp'}
144
145 wrapper = SshWrapper('date', 'git', 'admin', '3', 'False',
146 dummy_conf)
147
148 exit_code = wrapper.wrap()
149 assert exit_code == 0
150 assert git_run_mock.called
151 assert get_repo_store_mock.called
152 assert get_user_mock.called
153 invalidate_cache_mock.assert_called_once_with(repo_name)
154
155 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
156 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
157 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
158 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.GitServer.run')
159 def test_serve_git_invalidates_cache(
160 self, git_run_mock, get_repo_store_mock, get_user_mock,
161 invalidate_cache_mock, dummy_conf):
162 repo_name = None
163 git_run_mock.return_value = 0, True
164 get_user_mock.return_value = {repo_name: 'repository.admin'}
165 get_repo_store_mock.return_value = {'path': '/tmp'}
37 def test_get_connection_info(self, ssh_wrapper):
38 conn_info = ssh_wrapper.get_connection_info()
39 assert {'client_ip': '127.0.0.1',
40 'client_port': '22',
41 'server_ip': '10.0.0.1',
42 'server_port': '443'} == conn_info
166 43
167 wrapper = SshWrapper('date', 'git', 'admin', '3', 'False', dummy_conf)
168
169 exit_code = wrapper.wrap()
170 assert exit_code == 0
171 assert git_run_mock.called
172
173 assert get_repo_store_mock.called
174 assert get_user_mock.called
175 invalidate_cache_mock.assert_called_once_with(repo_name)
176
177
178 class TestServeSvn(object):
44 @pytest.mark.parametrize('command, vcs', [
45 ('xxx', None),
46 ('svnserve -t', 'svn'),
47 ('hg -R repo serve --stdio', 'hg'),
48 ('git-receive-pack \'repo.git\'', 'git'),
179 49
180 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.invalidate_cache')
181 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_user_permissions')
182 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.RhodeCodeApiClient.get_repo_store')
183 @mock.patch('rhodecode.apps.ssh_support.lib.ssh_wrapper.SubversionServer.run')
184 def test_serve_creates_svn_instance(
185 self, svn_run_mock, get_repo_store_mock, get_user_mock,
186 invalidate_cache_mock, dummy_conf):
187
188 repo_name = None
189 svn_run_mock.return_value = 0, True
190 get_user_mock.return_value = {repo_name: 'repository.admin'}
191 get_repo_store_mock.return_value = {'path': '/tmp'}
192
193 wrapper = SshWrapper('date', 'svn', 'admin', '3', 'False', dummy_conf)
194
195 exit_code = wrapper.wrap()
196 assert exit_code == 0
197 assert svn_run_mock.called
198
199 assert get_repo_store_mock.called
200 assert get_user_mock.called
201 invalidate_cache_mock.assert_called_once_with(repo_name)
50 ])
51 def test_get_repo_details(self, ssh_wrapper, command, vcs):
52 ssh_wrapper.command = command
53 vcs_type, repo_name, mode = ssh_wrapper.get_repo_details(mode='auto')
54 assert vcs_type == vcs
@@ -1,635 +1,634 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 32 import pyramid.threadlocal
33 33
34 34 from paste.auth.basic import AuthBasicAuthenticator
35 35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37 37
38 38 import rhodecode
39 39 from rhodecode.authentication.base import VCS_TYPE
40 40 from rhodecode.lib import auth, utils2
41 41 from rhodecode.lib import helpers as h
42 42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 43 from rhodecode.lib.exceptions import UserCreationError
44 44 from rhodecode.lib.utils import (
45 45 get_repo_slug, set_rhodecode_config, password_changed,
46 46 get_enabled_hook_classes)
47 47 from rhodecode.lib.utils2 import (
48 48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
49 49 from rhodecode.model import meta
50 50 from rhodecode.model.db import Repository, User, ChangesetComment
51 51 from rhodecode.model.notification import NotificationModel
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
54 54
55 55 # NOTE(marcink): remove after base controller is no longer required
56 56 from pylons.controllers import WSGIController
57 57 from pylons.i18n import translation
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 # hack to make the migration to pyramid easier
63 63 def render(template_name, extra_vars=None, cache_key=None,
64 64 cache_type=None, cache_expire=None):
65 65 """Render a template with Mako
66 66
67 67 Accepts the cache options ``cache_key``, ``cache_type``, and
68 68 ``cache_expire``.
69 69
70 70 """
71 71 from pylons.templating import literal
72 72 from pylons.templating import cached_template, pylons_globals
73 73
74 74 # Create a render callable for the cache function
75 75 def render_template():
76 76 # Pull in extra vars if needed
77 77 globs = extra_vars or {}
78 78
79 79 # Second, get the globals
80 80 globs.update(pylons_globals())
81 81
82 82 globs['_ungettext'] = globs['ungettext']
83 83 # Grab a template reference
84 84 template = globs['app_globals'].mako_lookup.get_template(template_name)
85 85
86 86 return literal(template.render_unicode(**globs))
87 87
88 88 return cached_template(template_name, render_template, cache_key=cache_key,
89 89 cache_type=cache_type, cache_expire=cache_expire)
90 90
91 91 def _filter_proxy(ip):
92 92 """
93 93 Passed in IP addresses in HEADERS can be in a special format of multiple
94 94 ips. Those comma separated IPs are passed from various proxies in the
95 95 chain of request processing. The left-most being the original client.
96 96 We only care about the first IP which came from the org. client.
97 97
98 98 :param ip: ip string from headers
99 99 """
100 100 if ',' in ip:
101 101 _ips = ip.split(',')
102 102 _first_ip = _ips[0].strip()
103 103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
104 104 return _first_ip
105 105 return ip
106 106
107 107
108 108 def _filter_port(ip):
109 109 """
110 110 Removes a port from ip, there are 4 main cases to handle here.
111 111 - ipv4 eg. 127.0.0.1
112 112 - ipv6 eg. ::1
113 113 - ipv4+port eg. 127.0.0.1:8080
114 114 - ipv6+port eg. [::1]:8080
115 115
116 116 :param ip:
117 117 """
118 118 def is_ipv6(ip_addr):
119 119 if hasattr(socket, 'inet_pton'):
120 120 try:
121 121 socket.inet_pton(socket.AF_INET6, ip_addr)
122 122 except socket.error:
123 123 return False
124 124 else:
125 125 # fallback to ipaddress
126 126 try:
127 127 ipaddress.IPv6Address(safe_unicode(ip_addr))
128 128 except Exception:
129 129 return False
130 130 return True
131 131
132 132 if ':' not in ip: # must be ipv4 pure ip
133 133 return ip
134 134
135 135 if '[' in ip and ']' in ip: # ipv6 with port
136 136 return ip.split(']')[0][1:].lower()
137 137
138 138 # must be ipv6 or ipv4 with port
139 139 if is_ipv6(ip):
140 140 return ip
141 141 else:
142 142 ip, _port = ip.split(':')[:2] # means ipv4+port
143 143 return ip
144 144
145 145
146 146 def get_ip_addr(environ):
147 147 proxy_key = 'HTTP_X_REAL_IP'
148 148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
149 149 def_key = 'REMOTE_ADDR'
150 150 _filters = lambda x: _filter_port(_filter_proxy(x))
151 151
152 152 ip = environ.get(proxy_key)
153 153 if ip:
154 154 return _filters(ip)
155 155
156 156 ip = environ.get(proxy_key2)
157 157 if ip:
158 158 return _filters(ip)
159 159
160 160 ip = environ.get(def_key, '0.0.0.0')
161 161 return _filters(ip)
162 162
163 163
164 164 def get_server_ip_addr(environ, log_errors=True):
165 165 hostname = environ.get('SERVER_NAME')
166 166 try:
167 167 return socket.gethostbyname(hostname)
168 168 except Exception as e:
169 169 if log_errors:
170 170 # in some cases this lookup is not possible, and we don't want to
171 171 # make it an exception in logs
172 172 log.exception('Could not retrieve server ip address: %s', e)
173 173 return hostname
174 174
175 175
176 176 def get_server_port(environ):
177 177 return environ.get('SERVER_PORT')
178 178
179 179
180 180 def get_access_path(environ):
181 181 path = environ.get('PATH_INFO')
182 182 org_req = environ.get('pylons.original_request')
183 183 if org_req:
184 184 path = org_req.environ.get('PATH_INFO')
185 185 return path
186 186
187 187
188 188 def get_user_agent(environ):
189 189 return environ.get('HTTP_USER_AGENT')
190 190
191 191
192 192 def vcs_operation_context(
193 193 environ, repo_name, username, action, scm, check_locking=True,
194 194 is_shadow_repo=False):
195 195 """
196 196 Generate the context for a vcs operation, e.g. push or pull.
197 197
198 198 This context is passed over the layers so that hooks triggered by the
199 199 vcs operation know details like the user, the user's IP address etc.
200 200
201 201 :param check_locking: Allows to switch of the computation of the locking
202 202 data. This serves mainly the need of the simplevcs middleware to be
203 203 able to disable this for certain operations.
204 204
205 205 """
206 206 # Tri-state value: False: unlock, None: nothing, True: lock
207 207 make_lock = None
208 208 locked_by = [None, None, None]
209 209 is_anonymous = username == User.DEFAULT_USER
210 210 if not is_anonymous and check_locking:
211 211 log.debug('Checking locking on repository "%s"', repo_name)
212 212 user = User.get_by_username(username)
213 213 repo = Repository.get_by_repo_name(repo_name)
214 214 make_lock, __, locked_by = repo.get_locking_state(
215 215 action, user.user_id)
216 216
217 217 settings_model = VcsSettingsModel(repo=repo_name)
218 218 ui_settings = settings_model.get_ui_settings()
219 219
220 220 extras = {
221 221 'ip': get_ip_addr(environ),
222 222 'username': username,
223 223 'action': action,
224 224 'repository': repo_name,
225 225 'scm': scm,
226 226 'config': rhodecode.CONFIG['__file__'],
227 227 'make_lock': make_lock,
228 228 'locked_by': locked_by,
229 229 'server_url': utils2.get_server_url(environ),
230 230 'user_agent': get_user_agent(environ),
231 231 'hooks': get_enabled_hook_classes(ui_settings),
232 232 'is_shadow_repo': is_shadow_repo,
233 233 }
234 234 return extras
235 235
236 236
237 237 class BasicAuth(AuthBasicAuthenticator):
238 238
239 239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
240 240 initial_call_detection=False, acl_repo_name=None):
241 241 self.realm = realm
242 242 self.initial_call = initial_call_detection
243 243 self.authfunc = authfunc
244 244 self.registry = registry
245 245 self.acl_repo_name = acl_repo_name
246 246 self._rc_auth_http_code = auth_http_code
247 247
248 248 def _get_response_from_code(self, http_code):
249 249 try:
250 250 return get_exception(safe_int(http_code))
251 251 except Exception:
252 252 log.exception('Failed to fetch response for code %s' % http_code)
253 253 return HTTPForbidden
254 254
255 255 def get_rc_realm(self):
256 256 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
257 257
258 258 def build_authentication(self):
259 259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
260 260 if self._rc_auth_http_code and not self.initial_call:
261 261 # return alternative HTTP code if alternative http return code
262 262 # is specified in RhodeCode config, but ONLY if it's not the
263 263 # FIRST call
264 264 custom_response_klass = self._get_response_from_code(
265 265 self._rc_auth_http_code)
266 266 return custom_response_klass(headers=head)
267 267 return HTTPUnauthorized(headers=head)
268 268
269 269 def authenticate(self, environ):
270 270 authorization = AUTHORIZATION(environ)
271 271 if not authorization:
272 272 return self.build_authentication()
273 273 (authmeth, auth) = authorization.split(' ', 1)
274 274 if 'basic' != authmeth.lower():
275 275 return self.build_authentication()
276 276 auth = auth.strip().decode('base64')
277 277 _parts = auth.split(':', 1)
278 278 if len(_parts) == 2:
279 279 username, password = _parts
280 280 auth_data = self.authfunc(
281 281 username, password, environ, VCS_TYPE,
282 282 registry=self.registry, acl_repo_name=self.acl_repo_name)
283 283 if auth_data:
284 284 return {'username': username, 'auth_data': auth_data}
285 285 if username and password:
286 286 # we mark that we actually executed authentication once, at
287 287 # that point we can use the alternative auth code
288 288 self.initial_call = False
289 289
290 290 return self.build_authentication()
291 291
292 292 __call__ = authenticate
293 293
294 294
295 295 def calculate_version_hash(config):
296 296 return md5(
297 297 config.get('beaker.session.secret', '') +
298 298 rhodecode.__version__)[:8]
299 299
300 300
301 301 def get_current_lang(request):
302 302 # NOTE(marcink): remove after pyramid move
303 303 try:
304 304 return translation.get_lang()[0]
305 305 except:
306 306 pass
307 307
308 308 return getattr(request, '_LOCALE_', request.locale_name)
309 309
310 310
311 311 def attach_context_attributes(context, request, user_id):
312 312 """
313 313 Attach variables into template context called `c`, please note that
314 314 request could be pylons or pyramid request in here.
315 315 """
316 316 # NOTE(marcink): remove check after pyramid migration
317 317 if hasattr(request, 'registry'):
318 318 config = request.registry.settings
319 319 else:
320 320 from pylons import config
321 321
322 322 rc_config = SettingsModel().get_all_settings(cache=True)
323 323
324 324 context.rhodecode_version = rhodecode.__version__
325 325 context.rhodecode_edition = config.get('rhodecode.edition')
326 326 # unique secret + version does not leak the version but keep consistency
327 327 context.rhodecode_version_hash = calculate_version_hash(config)
328 328
329 329 # Default language set for the incoming request
330 330 context.language = get_current_lang(request)
331 331
332 332 # Visual options
333 333 context.visual = AttributeDict({})
334 334
335 335 # DB stored Visual Items
336 336 context.visual.show_public_icon = str2bool(
337 337 rc_config.get('rhodecode_show_public_icon'))
338 338 context.visual.show_private_icon = str2bool(
339 339 rc_config.get('rhodecode_show_private_icon'))
340 340 context.visual.stylify_metatags = str2bool(
341 341 rc_config.get('rhodecode_stylify_metatags'))
342 342 context.visual.dashboard_items = safe_int(
343 343 rc_config.get('rhodecode_dashboard_items', 100))
344 344 context.visual.admin_grid_items = safe_int(
345 345 rc_config.get('rhodecode_admin_grid_items', 100))
346 346 context.visual.repository_fields = str2bool(
347 347 rc_config.get('rhodecode_repository_fields'))
348 348 context.visual.show_version = str2bool(
349 349 rc_config.get('rhodecode_show_version'))
350 350 context.visual.use_gravatar = str2bool(
351 351 rc_config.get('rhodecode_use_gravatar'))
352 352 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
353 353 context.visual.default_renderer = rc_config.get(
354 354 'rhodecode_markup_renderer', 'rst')
355 355 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
356 356 context.visual.rhodecode_support_url = \
357 357 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
358 358
359 359 context.visual.affected_files_cut_off = 60
360 360
361 361 context.pre_code = rc_config.get('rhodecode_pre_code')
362 362 context.post_code = rc_config.get('rhodecode_post_code')
363 363 context.rhodecode_name = rc_config.get('rhodecode_title')
364 364 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
365 365 # if we have specified default_encoding in the request, it has more
366 366 # priority
367 367 if request.GET.get('default_encoding'):
368 368 context.default_encodings.insert(0, request.GET.get('default_encoding'))
369 369 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
370 370
371 371 # INI stored
372 372 context.labs_active = str2bool(
373 373 config.get('labs_settings_active', 'false'))
374 374 context.visual.allow_repo_location_change = str2bool(
375 375 config.get('allow_repo_location_change', True))
376 376 context.visual.allow_custom_hooks_settings = str2bool(
377 377 config.get('allow_custom_hooks_settings', True))
378 378 context.debug_style = str2bool(config.get('debug_style', False))
379 379
380 380 context.rhodecode_instanceid = config.get('instance_id')
381 381
382 382 context.visual.cut_off_limit_diff = safe_int(
383 383 config.get('cut_off_limit_diff'))
384 384 context.visual.cut_off_limit_file = safe_int(
385 385 config.get('cut_off_limit_file'))
386 386
387 387 # AppEnlight
388 388 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
389 389 context.appenlight_api_public_key = config.get(
390 390 'appenlight.api_public_key', '')
391 391 context.appenlight_server_url = config.get('appenlight.server_url', '')
392 392
393 393 # JS template context
394 394 context.template_context = {
395 395 'repo_name': None,
396 396 'repo_type': None,
397 397 'repo_landing_commit': None,
398 398 'rhodecode_user': {
399 399 'username': None,
400 400 'email': None,
401 401 'notification_status': False
402 402 },
403 403 'visual': {
404 404 'default_renderer': None
405 405 },
406 406 'commit_data': {
407 407 'commit_id': None
408 408 },
409 409 'pull_request_data': {'pull_request_id': None},
410 410 'timeago': {
411 411 'refresh_time': 120 * 1000,
412 412 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
413 413 },
414 414 'pyramid_dispatch': {
415 415
416 416 },
417 417 'extra': {'plugins': {}}
418 418 }
419 419 # END CONFIG VARS
420 420
421 421 # TODO: This dosn't work when called from pylons compatibility tween.
422 422 # Fix this and remove it from base controller.
423 423 # context.repo_name = get_repo_slug(request) # can be empty
424 424
425 425 diffmode = 'sideside'
426 426 if request.GET.get('diffmode'):
427 427 if request.GET['diffmode'] == 'unified':
428 428 diffmode = 'unified'
429 429 elif request.session.get('diffmode'):
430 430 diffmode = request.session['diffmode']
431 431
432 432 context.diffmode = diffmode
433 433
434 434 if request.session.get('diffmode') != diffmode:
435 435 request.session['diffmode'] = diffmode
436 436
437 437 context.csrf_token = auth.get_csrf_token(session=request.session)
438 438 context.backends = rhodecode.BACKENDS.keys()
439 439 context.backends.sort()
440 440 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
441 441
442 442 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
443 443 # given request will ALWAYS be pyramid one
444 444 pyramid_request = pyramid.threadlocal.get_current_request()
445 445 context.pyramid_request = pyramid_request
446 446
447 447 # web case
448 448 if hasattr(pyramid_request, 'user'):
449 449 context.auth_user = pyramid_request.user
450 450 context.rhodecode_user = pyramid_request.user
451 451
452 452 # api case
453 453 if hasattr(pyramid_request, 'rpc_user'):
454 454 context.auth_user = pyramid_request.rpc_user
455 455 context.rhodecode_user = pyramid_request.rpc_user
456 456
457 457 # attach the whole call context to the request
458 458 request.call_context = context
459 459
460 460
461 461 def get_auth_user(request):
462 462 environ = request.environ
463 463 session = request.session
464 464
465 465 ip_addr = get_ip_addr(environ)
466 466 # make sure that we update permissions each time we call controller
467 467 _auth_token = (request.GET.get('auth_token', '') or
468 468 request.GET.get('api_key', ''))
469 469
470 470 if _auth_token:
471 471 # when using API_KEY we assume user exists, and
472 472 # doesn't need auth based on cookies.
473 473 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
474 474 authenticated = False
475 475 else:
476 476 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
477 477 try:
478 478 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
479 479 ip_addr=ip_addr)
480 480 except UserCreationError as e:
481 481 h.flash(e, 'error')
482 482 # container auth or other auth functions that create users
483 483 # on the fly can throw this exception signaling that there's
484 484 # issue with user creation, explanation should be provided
485 485 # in Exception itself. We then create a simple blank
486 486 # AuthUser
487 487 auth_user = AuthUser(ip_addr=ip_addr)
488 488
489 489 if password_changed(auth_user, session):
490 490 session.invalidate()
491 491 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
492 492 auth_user = AuthUser(ip_addr=ip_addr)
493 493
494 494 authenticated = cookie_store.get('is_authenticated')
495 495
496 496 if not auth_user.is_authenticated and auth_user.is_user_object:
497 497 # user is not authenticated and not empty
498 498 auth_user.set_authenticated(authenticated)
499 499
500 500 return auth_user
501 501
502 502
503 503 class BaseController(WSGIController):
504 504
505 505 def __before__(self):
506 506 """
507 507 __before__ is called before controller methods and after __call__
508 508 """
509 509 # on each call propagate settings calls into global settings.
510 510 from pylons import config
511 511 from pylons import tmpl_context as c, request, url
512 512 set_rhodecode_config(config)
513 513 attach_context_attributes(c, request, self._rhodecode_user.user_id)
514 514
515 515 # TODO: Remove this when fixed in attach_context_attributes()
516 516 c.repo_name = get_repo_slug(request) # can be empty
517 517
518 518 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
519 519 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
520 520 self.sa = meta.Session
521 521 self.scm_model = ScmModel(self.sa)
522 522
523 523 # set user language
524 524 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
525 525 if user_lang:
526 526 translation.set_lang(user_lang)
527 527 log.debug('set language to %s for user %s',
528 528 user_lang, self._rhodecode_user)
529 529
530 530 def _dispatch_redirect(self, with_url, environ, start_response):
531 531 from webob.exc import HTTPFound
532 532 resp = HTTPFound(with_url)
533 533 environ['SCRIPT_NAME'] = '' # handle prefix middleware
534 534 environ['PATH_INFO'] = with_url
535 535 return resp(environ, start_response)
536 536
537 537 def __call__(self, environ, start_response):
538 538 """Invoke the Controller"""
539 539 # WSGIController.__call__ dispatches to the Controller method
540 540 # the request is routed to. This routing information is
541 541 # available in environ['pylons.routes_dict']
542 542 from rhodecode.lib import helpers as h
543 543 from pylons import tmpl_context as c, request, url
544 544
545 545 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
546 546 if environ.get('debugtoolbar.wants_pylons_context', False):
547 547 environ['debugtoolbar.pylons_context'] = c._current_obj()
548 548
549 549 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
550 550 environ['pylons.routes_dict']['action']])
551 551
552 552 self.rc_config = SettingsModel().get_all_settings(cache=True)
553 553 self.ip_addr = get_ip_addr(environ)
554 554
555 555 # The rhodecode auth user is looked up and passed through the
556 556 # environ by the pylons compatibility tween in pyramid.
557 557 # So we can just grab it from there.
558 558 auth_user = environ['rc_auth_user']
559 559
560 560 # set globals for auth user
561 561 request.user = auth_user
562 562 self._rhodecode_user = auth_user
563 563
564 564 log.info('IP: %s User: %s accessed %s [%s]' % (
565 565 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
566 566 _route_name)
567 567 )
568 568
569 569 user_obj = auth_user.get_instance()
570 570 if user_obj and user_obj.user_data.get('force_password_change'):
571 571 h.flash('You are required to change your password', 'warning',
572 572 ignore_duplicate=True)
573 573 return self._dispatch_redirect(
574 574 url('my_account_password'), environ, start_response)
575 575
576 576 return WSGIController.__call__(self, environ, start_response)
577 577
578 578
579 579 def h_filter(s):
580 580 """
581 581 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
582 582 we wrap this with additional functionality that converts None to empty
583 583 strings
584 584 """
585 585 if s is None:
586 586 return markupsafe.Markup()
587 587 return markupsafe.escape(s)
588 588
589 589
590 590 def add_events_routes(config):
591 591 """
592 592 Adds routing that can be used in events. Because some events are triggered
593 593 outside of pyramid context, we need to bootstrap request with some
594 594 routing registered
595 595 """
596 596 config.add_route(name='home', pattern='/')
597 597
598 598 config.add_route(name='repo_summary', pattern='/{repo_name}')
599 599 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
600 600 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
601 601
602 602 config.add_route(name='pullrequest_show',
603 603 pattern='/{repo_name}/pull-request/{pull_request_id}')
604 604 config.add_route(name='pull_requests_global',
605 605 pattern='/pull-request/{pull_request_id}')
606 606
607 607 config.add_route(name='repo_commit',
608 608 pattern='/{repo_name}/changeset/{commit_id}')
609 609 config.add_route(name='repo_files',
610 610 pattern='/{repo_name}/files/{commit_id}/{f_path}')
611 611
612 612
613 613 def bootstrap_request(**kwargs):
614 614 import pyramid.testing
615 615
616 616 class TestRequest(pyramid.testing.DummyRequest):
617 617 application_url = kwargs.pop('application_url', 'http://example.com')
618 618 host = kwargs.pop('host', 'example.com:80')
619 619 domain = kwargs.pop('domain', 'example.com')
620 620
621 621 def translate(self, msg):
622 622 return msg
623 623
624 624 class TestDummySession(pyramid.testing.DummySession):
625 625 def save(*arg, **kw):
626 626 pass
627 627
628 628 request = TestRequest(**kwargs)
629 629 request.session = TestDummySession()
630 630
631
632 631 config = pyramid.testing.setUp(request=request)
633 632 add_events_routes(config)
634 633 return request
635 634
@@ -1,241 +1,242 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 json
22 22 import logging
23 23 import traceback
24 24 import threading
25 25 from BaseHTTPServer import BaseHTTPRequestHandler
26 26 from SocketServer import TCPServer
27 27
28 28 import rhodecode
29 29 from rhodecode.model import meta
30 30 from rhodecode.lib.base import bootstrap_request
31 31 from rhodecode.lib import hooks_base
32 32 from rhodecode.lib.utils2 import AttributeDict
33 33
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class HooksHttpHandler(BaseHTTPRequestHandler):
39 39 def do_POST(self):
40 40 method, extras = self._read_request()
41 41 try:
42 42 result = self._call_hook(method, extras)
43 43 except Exception as e:
44 44 exc_tb = traceback.format_exc()
45 45 result = {
46 46 'exception': e.__class__.__name__,
47 47 'exception_traceback': exc_tb,
48 48 'exception_args': e.args
49 49 }
50 50 self._write_response(result)
51 51
52 52 def _read_request(self):
53 53 length = int(self.headers['Content-Length'])
54 54 body = self.rfile.read(length).decode('utf-8')
55 55 data = json.loads(body)
56 56 return data['method'], data['extras']
57 57
58 58 def _write_response(self, result):
59 59 self.send_response(200)
60 60 self.send_header("Content-type", "text/json")
61 61 self.end_headers()
62 62 self.wfile.write(json.dumps(result))
63 63
64 64 def _call_hook(self, method, extras):
65 65 hooks = Hooks()
66 66 try:
67 67 result = getattr(hooks, method)(extras)
68 68 finally:
69 69 meta.Session.remove()
70 70 return result
71 71
72 72 def log_message(self, format, *args):
73 73 """
74 74 This is an overridden method of BaseHTTPRequestHandler which logs using
75 75 logging library instead of writing directly to stderr.
76 76 """
77 77
78 78 message = format % args
79 79
80 80 # TODO: mikhail: add different log levels support
81 81 log.debug(
82 82 "%s - - [%s] %s", self.client_address[0],
83 83 self.log_date_time_string(), message)
84 84
85 85
86 86 class DummyHooksCallbackDaemon(object):
87 87 def __init__(self):
88 88 self.hooks_module = Hooks.__module__
89 89
90 90 def __enter__(self):
91 91 log.debug('Running dummy hooks callback daemon')
92 92 return self
93 93
94 94 def __exit__(self, exc_type, exc_val, exc_tb):
95 95 log.debug('Exiting dummy hooks callback daemon')
96 96
97 97
98 98 class ThreadedHookCallbackDaemon(object):
99 99
100 100 _callback_thread = None
101 101 _daemon = None
102 102 _done = False
103 103
104 104 def __init__(self):
105 105 self._prepare()
106 106
107 107 def __enter__(self):
108 108 self._run()
109 109 return self
110 110
111 111 def __exit__(self, exc_type, exc_val, exc_tb):
112 112 log.debug('Callback daemon exiting now...')
113 113 self._stop()
114 114
115 115 def _prepare(self):
116 116 raise NotImplementedError()
117 117
118 118 def _run(self):
119 119 raise NotImplementedError()
120 120
121 121 def _stop(self):
122 122 raise NotImplementedError()
123 123
124 124
125 125 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
126 126 """
127 127 Context manager which will run a callback daemon in a background thread.
128 128 """
129 129
130 130 hooks_uri = None
131 131
132 132 IP_ADDRESS = '127.0.0.1'
133 133
134 134 # From Python docs: Polling reduces our responsiveness to a shutdown
135 135 # request and wastes cpu at all other times.
136 136 POLL_INTERVAL = 0.1
137 137
138 138 def _prepare(self):
139 139 log.debug("Preparing HTTP callback daemon and registering hook object")
140 140
141 141 self._done = False
142 142 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
143 143 _, port = self._daemon.server_address
144 144 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
145 145
146 146 log.debug("Hooks uri is: %s", self.hooks_uri)
147 147
148 148 def _run(self):
149 149 log.debug("Running event loop of callback daemon in background thread")
150 150 callback_thread = threading.Thread(
151 151 target=self._daemon.serve_forever,
152 152 kwargs={'poll_interval': self.POLL_INTERVAL})
153 153 callback_thread.daemon = True
154 154 callback_thread.start()
155 155 self._callback_thread = callback_thread
156 156
157 157 def _stop(self):
158 158 log.debug("Waiting for background thread to finish.")
159 159 self._daemon.shutdown()
160 160 self._callback_thread.join()
161 161 self._daemon = None
162 162 self._callback_thread = None
163 163
164 164
165 165 def prepare_callback_daemon(extras, protocol, use_direct_calls):
166 166 callback_daemon = None
167 167
168 168 if use_direct_calls:
169 169 callback_daemon = DummyHooksCallbackDaemon()
170 170 extras['hooks_module'] = callback_daemon.hooks_module
171 171 else:
172 172 if protocol == 'http':
173 173 callback_daemon = HttpHooksCallbackDaemon()
174 174 else:
175 175 log.error('Unsupported callback daemon protocol "%s"', protocol)
176 176 raise Exception('Unsupported callback daemon protocol.')
177 177
178 178 extras['hooks_uri'] = callback_daemon.hooks_uri
179 179 extras['hooks_protocol'] = protocol
180 180
181 181 log.debug('Prepared a callback daemon: %s', callback_daemon)
182 182 return callback_daemon, extras
183 183
184 184
185 185 class Hooks(object):
186 186 """
187 187 Exposes the hooks for remote call backs
188 188 """
189 189
190 190 def repo_size(self, extras):
191 191 log.debug("Called repo_size of %s object", self)
192 192 return self._call_hook(hooks_base.repo_size, extras)
193 193
194 194 def pre_pull(self, extras):
195 195 log.debug("Called pre_pull of %s object", self)
196 196 return self._call_hook(hooks_base.pre_pull, extras)
197 197
198 198 def post_pull(self, extras):
199 199 log.debug("Called post_pull of %s object", self)
200 200 return self._call_hook(hooks_base.post_pull, extras)
201 201
202 202 def pre_push(self, extras):
203 203 log.debug("Called pre_push of %s object", self)
204 204 return self._call_hook(hooks_base.pre_push, extras)
205 205
206 206 def post_push(self, extras):
207 207 log.debug("Called post_push of %s object", self)
208 208 return self._call_hook(hooks_base.post_push, extras)
209 209
210 210 def _call_hook(self, hook, extras):
211 211 extras = AttributeDict(extras)
212 server_url = extras['server_url']
212 213
213 extras.request = bootstrap_request(
214 application_url=extras['server_url'])
214 extras.request = bootstrap_request(application_url=server_url)
215 215
216 216 try:
217 217 result = hook(extras)
218 218 except Exception as error:
219 219 exc_tb = traceback.format_exc()
220 220 log.exception('Exception when handling hook %s', hook)
221 221 error_args = error.args
222 222 return {
223 223 'status': 128,
224 224 'output': '',
225 225 'exception': type(error).__name__,
226 226 'exception_traceback': exc_tb,
227 227 'exception_args': error_args,
228 228 }
229 229 finally:
230 230 meta.Session.remove()
231 231
232 log.debug('Got hook call response %s', result)
232 233 return {
233 234 'status': result.status,
234 235 'output': result.output,
235 236 }
236 237
237 238 def __enter__(self):
238 239 return self
239 240
240 241 def __exit__(self, exc_type, exc_val, exc_tb):
241 242 pass
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now