##// END OF EJS Templates
git-lfs: fixed bug #5399 git-lfs application failed to generate HTTPS urls properly.
dan -
r3781:b4126386 default
parent child Browse files
Show More
@@ -1,155 +1,156 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SimpleGit middleware for handling git protocol request (push/clone etc.)
22 SimpleGit middleware for handling git protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25 import os
25 import os
26 import re
26 import re
27 import logging
27 import logging
28 import urlparse
28 import urlparse
29
29
30 import rhodecode
30 import rhodecode
31 from rhodecode.lib import utils
31 from rhodecode.lib import utils
32 from rhodecode.lib import utils2
32 from rhodecode.lib import utils2
33 from rhodecode.lib.middleware import simplevcs
33 from rhodecode.lib.middleware import simplevcs
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 GIT_PROTO_PAT = re.compile(
38 GIT_PROTO_PAT = re.compile(
39 r'^/(.+)/(info/refs|info/lfs/(.+)|git-upload-pack|git-receive-pack)')
39 r'^/(.+)/(info/refs|info/lfs/(.+)|git-upload-pack|git-receive-pack)')
40 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
40 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
41
41
42
42
43 def default_lfs_store():
43 def default_lfs_store():
44 """
44 """
45 Default lfs store location, it's consistent with Mercurials large file
45 Default lfs store location, it's consistent with Mercurials large file
46 store which is in .cache/largefiles
46 store which is in .cache/largefiles
47 """
47 """
48 from rhodecode.lib.vcs.backends.git import lfs_store
48 from rhodecode.lib.vcs.backends.git import lfs_store
49 user_home = os.path.expanduser("~")
49 user_home = os.path.expanduser("~")
50 return lfs_store(user_home)
50 return lfs_store(user_home)
51
51
52
52
53 class SimpleGit(simplevcs.SimpleVCS):
53 class SimpleGit(simplevcs.SimpleVCS):
54
54
55 SCM = 'git'
55 SCM = 'git'
56
56
57 def _get_repository_name(self, environ):
57 def _get_repository_name(self, environ):
58 """
58 """
59 Gets repository name out of PATH_INFO header
59 Gets repository name out of PATH_INFO header
60
60
61 :param environ: environ where PATH_INFO is stored
61 :param environ: environ where PATH_INFO is stored
62 """
62 """
63 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
63 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
64 # for GIT LFS, and bare format strip .git suffix from names
64 # for GIT LFS, and bare format strip .git suffix from names
65 if repo_name.endswith('.git'):
65 if repo_name.endswith('.git'):
66 repo_name = repo_name[:-4]
66 repo_name = repo_name[:-4]
67 return repo_name
67 return repo_name
68
68
69 def _get_lfs_action(self, path, request_method):
69 def _get_lfs_action(self, path, request_method):
70 """
70 """
71 return an action based on LFS requests type.
71 return an action based on LFS requests type.
72 Those routes are handled inside vcsserver app.
72 Those routes are handled inside vcsserver app.
73
73
74 batch -> POST to /info/lfs/objects/batch => PUSH/PULL
74 batch -> POST to /info/lfs/objects/batch => PUSH/PULL
75 batch is based on the `operation.
75 batch is based on the `operation.
76 that could be download or upload, but those are only
76 that could be download or upload, but those are only
77 instructions to fetch so we return pull always
77 instructions to fetch so we return pull always
78
78
79 download -> GET to /info/lfs/{oid} => PULL
79 download -> GET to /info/lfs/{oid} => PULL
80 upload -> PUT to /info/lfs/{oid} => PUSH
80 upload -> PUT to /info/lfs/{oid} => PUSH
81
81
82 verification -> POST to /info/lfs/verify => PULL
82 verification -> POST to /info/lfs/verify => PULL
83
83
84 """
84 """
85
85
86 match_obj = GIT_LFS_PROTO_PAT.match(path)
86 match_obj = GIT_LFS_PROTO_PAT.match(path)
87 _parts = match_obj.groups()
87 _parts = match_obj.groups()
88 repo_name, path, operation = _parts
88 repo_name, path, operation = _parts
89 log.debug(
89 log.debug(
90 'LFS: detecting operation based on following '
90 'LFS: detecting operation based on following '
91 'data: %s, req_method:%s', _parts, request_method)
91 'data: %s, req_method:%s', _parts, request_method)
92
92
93 if operation == 'verify':
93 if operation == 'verify':
94 return 'pull'
94 return 'pull'
95 elif operation == 'objects/batch':
95 elif operation == 'objects/batch':
96 # batch sends back instructions for API to dl/upl we report it
96 # batch sends back instructions for API to dl/upl we report it
97 # as pull
97 # as pull
98 if request_method == 'POST':
98 if request_method == 'POST':
99 return 'pull'
99 return 'pull'
100
100
101 elif operation:
101 elif operation:
102 # probably a OID, upload is PUT, download a GET
102 # probably a OID, upload is PUT, download a GET
103 if request_method == 'GET':
103 if request_method == 'GET':
104 return 'pull'
104 return 'pull'
105 else:
105 else:
106 return 'push'
106 return 'push'
107
107
108 # if default not found require push, as action
108 # if default not found require push, as action
109 return 'push'
109 return 'push'
110
110
111 _ACTION_MAPPING = {
111 _ACTION_MAPPING = {
112 'git-receive-pack': 'push',
112 'git-receive-pack': 'push',
113 'git-upload-pack': 'pull',
113 'git-upload-pack': 'pull',
114 }
114 }
115
115
116 def _get_action(self, environ):
116 def _get_action(self, environ):
117 """
117 """
118 Maps git request commands into a pull or push command.
118 Maps git request commands into a pull or push command.
119 In case of unknown/unexpected data, it returns 'pull' to be safe.
119 In case of unknown/unexpected data, it returns 'pull' to be safe.
120
120
121 :param environ:
121 :param environ:
122 """
122 """
123 path = environ['PATH_INFO']
123 path = environ['PATH_INFO']
124
124
125 if path.endswith('/info/refs'):
125 if path.endswith('/info/refs'):
126 query = urlparse.parse_qs(environ['QUERY_STRING'])
126 query = urlparse.parse_qs(environ['QUERY_STRING'])
127 service_cmd = query.get('service', [''])[0]
127 service_cmd = query.get('service', [''])[0]
128 return self._ACTION_MAPPING.get(service_cmd, 'pull')
128 return self._ACTION_MAPPING.get(service_cmd, 'pull')
129
129
130 elif GIT_LFS_PROTO_PAT.match(environ['PATH_INFO']):
130 elif GIT_LFS_PROTO_PAT.match(environ['PATH_INFO']):
131 return self._get_lfs_action(
131 return self._get_lfs_action(
132 environ['PATH_INFO'], environ['REQUEST_METHOD'])
132 environ['PATH_INFO'], environ['REQUEST_METHOD'])
133
133
134 elif path.endswith('/git-receive-pack'):
134 elif path.endswith('/git-receive-pack'):
135 return 'push'
135 return 'push'
136 elif path.endswith('/git-upload-pack'):
136 elif path.endswith('/git-upload-pack'):
137 return 'pull'
137 return 'pull'
138
138
139 return 'pull'
139 return 'pull'
140
140
141 def _create_wsgi_app(self, repo_path, repo_name, config):
141 def _create_wsgi_app(self, repo_path, repo_name, config):
142 return self.scm_app.create_git_wsgi_app(
142 return self.scm_app.create_git_wsgi_app(
143 repo_path, repo_name, config)
143 repo_path, repo_name, config)
144
144
145 def _create_config(self, extras, repo_name):
145 def _create_config(self, extras, repo_name, scheme='http'):
146 extras['git_update_server_info'] = utils2.str2bool(
146 extras['git_update_server_info'] = utils2.str2bool(
147 rhodecode.CONFIG.get('git_update_server_info'))
147 rhodecode.CONFIG.get('git_update_server_info'))
148
148
149 config = utils.make_db_config(repo=repo_name)
149 config = utils.make_db_config(repo=repo_name)
150 custom_store = config.get('vcs_git_lfs', 'store_location')
150 custom_store = config.get('vcs_git_lfs', 'store_location')
151
151
152 extras['git_lfs_enabled'] = utils2.str2bool(
152 extras['git_lfs_enabled'] = utils2.str2bool(
153 config.get('vcs_git_lfs', 'enabled'))
153 config.get('vcs_git_lfs', 'enabled'))
154 extras['git_lfs_store_path'] = custom_store or default_lfs_store()
154 extras['git_lfs_store_path'] = custom_store or default_lfs_store()
155 extras['git_lfs_http_scheme'] = scheme
155 return extras
156 return extras
@@ -1,160 +1,160 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SimpleHG middleware for handling mercurial protocol request
22 SimpleHG middleware for handling mercurial protocol request
23 (push/clone etc.). It's implemented with basic auth function
23 (push/clone etc.). It's implemented with basic auth function
24 """
24 """
25
25
26 import logging
26 import logging
27 import urlparse
27 import urlparse
28 import urllib
28 import urllib
29
29
30 from rhodecode.lib import utils
30 from rhodecode.lib import utils
31 from rhodecode.lib.ext_json import json
31 from rhodecode.lib.ext_json import json
32 from rhodecode.lib.middleware import simplevcs
32 from rhodecode.lib.middleware import simplevcs
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 class SimpleHg(simplevcs.SimpleVCS):
37 class SimpleHg(simplevcs.SimpleVCS):
38
38
39 SCM = 'hg'
39 SCM = 'hg'
40
40
41 def _get_repository_name(self, environ):
41 def _get_repository_name(self, environ):
42 """
42 """
43 Gets repository name out of PATH_INFO header
43 Gets repository name out of PATH_INFO header
44
44
45 :param environ: environ where PATH_INFO is stored
45 :param environ: environ where PATH_INFO is stored
46 """
46 """
47 repo_name = environ['PATH_INFO']
47 repo_name = environ['PATH_INFO']
48 if repo_name and repo_name.startswith('/'):
48 if repo_name and repo_name.startswith('/'):
49 # remove only the first leading /
49 # remove only the first leading /
50 repo_name = repo_name[1:]
50 repo_name = repo_name[1:]
51 return repo_name.rstrip('/')
51 return repo_name.rstrip('/')
52
52
53 _ACTION_MAPPING = {
53 _ACTION_MAPPING = {
54 'changegroup': 'pull',
54 'changegroup': 'pull',
55 'changegroupsubset': 'pull',
55 'changegroupsubset': 'pull',
56 'getbundle': 'pull',
56 'getbundle': 'pull',
57 'stream_out': 'pull',
57 'stream_out': 'pull',
58 'listkeys': 'pull',
58 'listkeys': 'pull',
59 'between': 'pull',
59 'between': 'pull',
60 'branchmap': 'pull',
60 'branchmap': 'pull',
61 'branches': 'pull',
61 'branches': 'pull',
62 'clonebundles': 'pull',
62 'clonebundles': 'pull',
63 'capabilities': 'pull',
63 'capabilities': 'pull',
64 'debugwireargs': 'pull',
64 'debugwireargs': 'pull',
65 'heads': 'pull',
65 'heads': 'pull',
66 'lookup': 'pull',
66 'lookup': 'pull',
67 'hello': 'pull',
67 'hello': 'pull',
68 'known': 'pull',
68 'known': 'pull',
69
69
70 # largefiles
70 # largefiles
71 'putlfile': 'push',
71 'putlfile': 'push',
72 'getlfile': 'pull',
72 'getlfile': 'pull',
73 'statlfile': 'pull',
73 'statlfile': 'pull',
74 'lheads': 'pull',
74 'lheads': 'pull',
75
75
76 # evolve
76 # evolve
77 'evoext_obshashrange_v1': 'pull',
77 'evoext_obshashrange_v1': 'pull',
78 'evoext_obshash': 'pull',
78 'evoext_obshash': 'pull',
79 'evoext_obshash1': 'pull',
79 'evoext_obshash1': 'pull',
80
80
81 'unbundle': 'push',
81 'unbundle': 'push',
82 'pushkey': 'push',
82 'pushkey': 'push',
83 }
83 }
84
84
85 @classmethod
85 @classmethod
86 def _get_xarg_headers(cls, environ):
86 def _get_xarg_headers(cls, environ):
87 i = 1
87 i = 1
88 chunks = [] # gather chunks stored in multiple 'hgarg_N'
88 chunks = [] # gather chunks stored in multiple 'hgarg_N'
89 while True:
89 while True:
90 head = environ.get('HTTP_X_HGARG_{}'.format(i))
90 head = environ.get('HTTP_X_HGARG_{}'.format(i))
91 if not head:
91 if not head:
92 break
92 break
93 i += 1
93 i += 1
94 chunks.append(urllib.unquote_plus(head))
94 chunks.append(urllib.unquote_plus(head))
95 full_arg = ''.join(chunks)
95 full_arg = ''.join(chunks)
96 pref = 'cmds='
96 pref = 'cmds='
97 if full_arg.startswith(pref):
97 if full_arg.startswith(pref):
98 # strip the cmds= header defining our batch commands
98 # strip the cmds= header defining our batch commands
99 full_arg = full_arg[len(pref):]
99 full_arg = full_arg[len(pref):]
100 cmds = full_arg.split(';')
100 cmds = full_arg.split(';')
101 return cmds
101 return cmds
102
102
103 @classmethod
103 @classmethod
104 def _get_batch_cmd(cls, environ):
104 def _get_batch_cmd(cls, environ):
105 """
105 """
106 Handle batch command send commands. Those are ';' separated commands
106 Handle batch command send commands. Those are ';' separated commands
107 sent by batch command that server needs to execute. We need to extract
107 sent by batch command that server needs to execute. We need to extract
108 those, and map them to our ACTION_MAPPING to get all push/pull commands
108 those, and map them to our ACTION_MAPPING to get all push/pull commands
109 specified in the batch
109 specified in the batch
110 """
110 """
111 default = 'push'
111 default = 'push'
112 batch_cmds = []
112 batch_cmds = []
113 try:
113 try:
114 cmds = cls._get_xarg_headers(environ)
114 cmds = cls._get_xarg_headers(environ)
115 for pair in cmds:
115 for pair in cmds:
116 parts = pair.split(' ', 1)
116 parts = pair.split(' ', 1)
117 if len(parts) != 2:
117 if len(parts) != 2:
118 continue
118 continue
119 # entry should be in a format `key ARGS`
119 # entry should be in a format `key ARGS`
120 cmd, args = parts
120 cmd, args = parts
121 action = cls._ACTION_MAPPING.get(cmd, default)
121 action = cls._ACTION_MAPPING.get(cmd, default)
122 batch_cmds.append(action)
122 batch_cmds.append(action)
123 except Exception:
123 except Exception:
124 log.exception('Failed to extract batch commands operations')
124 log.exception('Failed to extract batch commands operations')
125
125
126 # in case we failed, (e.g malformed data) assume it's PUSH sub-command
126 # in case we failed, (e.g malformed data) assume it's PUSH sub-command
127 # for safety
127 # for safety
128 return batch_cmds or [default]
128 return batch_cmds or [default]
129
129
130 def _get_action(self, environ):
130 def _get_action(self, environ):
131 """
131 """
132 Maps mercurial request commands into a pull or push command.
132 Maps mercurial request commands into a pull or push command.
133 In case of unknown/unexpected data, it returns 'push' to be safe.
133 In case of unknown/unexpected data, it returns 'push' to be safe.
134
134
135 :param environ:
135 :param environ:
136 """
136 """
137 default = 'push'
137 default = 'push'
138 query = urlparse.parse_qs(environ['QUERY_STRING'],
138 query = urlparse.parse_qs(environ['QUERY_STRING'],
139 keep_blank_values=True)
139 keep_blank_values=True)
140
140
141 if 'cmd' in query:
141 if 'cmd' in query:
142 cmd = query['cmd'][0]
142 cmd = query['cmd'][0]
143 if cmd == 'batch':
143 if cmd == 'batch':
144 cmds = self._get_batch_cmd(environ)
144 cmds = self._get_batch_cmd(environ)
145 if 'push' in cmds:
145 if 'push' in cmds:
146 return 'push'
146 return 'push'
147 else:
147 else:
148 return 'pull'
148 return 'pull'
149 return self._ACTION_MAPPING.get(cmd, default)
149 return self._ACTION_MAPPING.get(cmd, default)
150
150
151 return default
151 return default
152
152
153 def _create_wsgi_app(self, repo_path, repo_name, config):
153 def _create_wsgi_app(self, repo_path, repo_name, config):
154 return self.scm_app.create_hg_wsgi_app(repo_path, repo_name, config)
154 return self.scm_app.create_hg_wsgi_app(repo_path, repo_name, config)
155
155
156 def _create_config(self, extras, repo_name):
156 def _create_config(self, extras, repo_name, scheme='http'):
157 config = utils.make_db_config(repo=repo_name)
157 config = utils.make_db_config(repo=repo_name)
158 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
158 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
159
159
160 return config.serialize()
160 return config.serialize()
@@ -1,228 +1,228 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import base64
21 import base64
22 import logging
22 import logging
23 import urllib
23 import urllib
24 import urlparse
24 import urlparse
25
25
26 import requests
26 import requests
27 from pyramid.httpexceptions import HTTPNotAcceptable
27 from pyramid.httpexceptions import HTTPNotAcceptable
28
28
29 from rhodecode.lib import rc_cache
29 from rhodecode.lib import rc_cache
30 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.utils import is_valid_repo
31 from rhodecode.lib.utils import is_valid_repo
32 from rhodecode.lib.utils2 import str2bool, safe_int
32 from rhodecode.lib.utils2 import str2bool, safe_int
33 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class SimpleSvnApp(object):
40 class SimpleSvnApp(object):
41 IGNORED_HEADERS = [
41 IGNORED_HEADERS = [
42 'connection', 'keep-alive', 'content-encoding',
42 'connection', 'keep-alive', 'content-encoding',
43 'transfer-encoding', 'content-length']
43 'transfer-encoding', 'content-length']
44 rc_extras = {}
44 rc_extras = {}
45
45
46 def __init__(self, config):
46 def __init__(self, config):
47 self.config = config
47 self.config = config
48
48
49 def __call__(self, environ, start_response):
49 def __call__(self, environ, start_response):
50 request_headers = self._get_request_headers(environ)
50 request_headers = self._get_request_headers(environ)
51 data = environ['wsgi.input']
51 data = environ['wsgi.input']
52 req_method = environ['REQUEST_METHOD']
52 req_method = environ['REQUEST_METHOD']
53 has_content_length = 'CONTENT_LENGTH' in environ
53 has_content_length = 'CONTENT_LENGTH' in environ
54 path_info = self._get_url(
54 path_info = self._get_url(
55 self.config.get('subversion_http_server_url', ''), environ['PATH_INFO'])
55 self.config.get('subversion_http_server_url', ''), environ['PATH_INFO'])
56 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
56 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
57 log.debug('Handling: %s method via `%s`', req_method, path_info)
57 log.debug('Handling: %s method via `%s`', req_method, path_info)
58
58
59 # stream control flag, based on request and content type...
59 # stream control flag, based on request and content type...
60 stream = False
60 stream = False
61
61
62 if req_method in ['MKCOL'] or has_content_length:
62 if req_method in ['MKCOL'] or has_content_length:
63 data_processed = False
63 data_processed = False
64 # read chunk to check if we have txn-with-props
64 # read chunk to check if we have txn-with-props
65 initial_data = data.read(1024)
65 initial_data = data.read(1024)
66 if initial_data.startswith('(create-txn-with-props'):
66 if initial_data.startswith('(create-txn-with-props'):
67 data = initial_data + data.read()
67 data = initial_data + data.read()
68 # store on-the-fly our rc_extra using svn revision properties
68 # store on-the-fly our rc_extra using svn revision properties
69 # those can be read later on in hooks executed so we have a way
69 # those can be read later on in hooks executed so we have a way
70 # to pass in the data into svn hooks
70 # to pass in the data into svn hooks
71 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
71 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
72 rc_data_len = len(rc_data)
72 rc_data_len = len(rc_data)
73 # header defines data length, and serialized data
73 # header defines data length, and serialized data
74 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
74 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
75 data = data[:-2] + skel + '))'
75 data = data[:-2] + skel + '))'
76 data_processed = True
76 data_processed = True
77
77
78 if not data_processed:
78 if not data_processed:
79 # NOTE(johbo): Avoid that we end up with sending the request in chunked
79 # NOTE(johbo): Avoid that we end up with sending the request in chunked
80 # transfer encoding (mainly on Gunicorn). If we know the content
80 # transfer encoding (mainly on Gunicorn). If we know the content
81 # length, then we should transfer the payload in one request.
81 # length, then we should transfer the payload in one request.
82 data = initial_data + data.read()
82 data = initial_data + data.read()
83
83
84 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
84 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
85 # NOTE(marcink): when getting/uploading files we want to STREAM content
85 # NOTE(marcink): when getting/uploading files we want to STREAM content
86 # back to the client/proxy instead of buffering it here...
86 # back to the client/proxy instead of buffering it here...
87 stream = True
87 stream = True
88
88
89 stream = stream
89 stream = stream
90 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
90 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
91 path_info, req_method, stream)
91 path_info, req_method, stream)
92 try:
92 try:
93 response = requests.request(
93 response = requests.request(
94 req_method, path_info,
94 req_method, path_info,
95 data=data, headers=request_headers, stream=stream)
95 data=data, headers=request_headers, stream=stream)
96 except requests.ConnectionError:
96 except requests.ConnectionError:
97 log.exception('ConnectionError occurred for endpoint %s', path_info)
97 log.exception('ConnectionError occurred for endpoint %s', path_info)
98 raise
98 raise
99
99
100 if response.status_code not in [200, 401]:
100 if response.status_code not in [200, 401]:
101 text = '\n{}'.format(response.text) if response.text else ''
101 text = '\n{}'.format(response.text) if response.text else ''
102 if response.status_code >= 500:
102 if response.status_code >= 500:
103 log.error('Got SVN response:%s with text:`%s`', response, text)
103 log.error('Got SVN response:%s with text:`%s`', response, text)
104 else:
104 else:
105 log.debug('Got SVN response:%s with text:`%s`', response, text)
105 log.debug('Got SVN response:%s with text:`%s`', response, text)
106 else:
106 else:
107 log.debug('got response code: %s', response.status_code)
107 log.debug('got response code: %s', response.status_code)
108
108
109 response_headers = self._get_response_headers(response.headers)
109 response_headers = self._get_response_headers(response.headers)
110
110
111 if response.headers.get('SVN-Txn-name'):
111 if response.headers.get('SVN-Txn-name'):
112 svn_tx_id = response.headers.get('SVN-Txn-name')
112 svn_tx_id = response.headers.get('SVN-Txn-name')
113 txn_id = rc_cache.utils.compute_key_from_params(
113 txn_id = rc_cache.utils.compute_key_from_params(
114 self.config['repository'], svn_tx_id)
114 self.config['repository'], svn_tx_id)
115 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
115 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
116 store_txn_id_data(txn_id, {'port': port})
116 store_txn_id_data(txn_id, {'port': port})
117
117
118 start_response(
118 start_response(
119 '{} {}'.format(response.status_code, response.reason),
119 '{} {}'.format(response.status_code, response.reason),
120 response_headers)
120 response_headers)
121 return response.iter_content(chunk_size=1024)
121 return response.iter_content(chunk_size=1024)
122
122
123 def _get_url(self, svn_http_server, path):
123 def _get_url(self, svn_http_server, path):
124 svn_http_server_url = (svn_http_server or '').rstrip('/')
124 svn_http_server_url = (svn_http_server or '').rstrip('/')
125 url_path = urlparse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
125 url_path = urlparse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
126 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
126 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
127 return url_path
127 return url_path
128
128
129 def _get_request_headers(self, environ):
129 def _get_request_headers(self, environ):
130 headers = {}
130 headers = {}
131
131
132 for key in environ:
132 for key in environ:
133 if not key.startswith('HTTP_'):
133 if not key.startswith('HTTP_'):
134 continue
134 continue
135 new_key = key.split('_')
135 new_key = key.split('_')
136 new_key = [k.capitalize() for k in new_key[1:]]
136 new_key = [k.capitalize() for k in new_key[1:]]
137 new_key = '-'.join(new_key)
137 new_key = '-'.join(new_key)
138 headers[new_key] = environ[key]
138 headers[new_key] = environ[key]
139
139
140 if 'CONTENT_TYPE' in environ:
140 if 'CONTENT_TYPE' in environ:
141 headers['Content-Type'] = environ['CONTENT_TYPE']
141 headers['Content-Type'] = environ['CONTENT_TYPE']
142
142
143 if 'CONTENT_LENGTH' in environ:
143 if 'CONTENT_LENGTH' in environ:
144 headers['Content-Length'] = environ['CONTENT_LENGTH']
144 headers['Content-Length'] = environ['CONTENT_LENGTH']
145
145
146 return headers
146 return headers
147
147
148 def _get_response_headers(self, headers):
148 def _get_response_headers(self, headers):
149 headers = [
149 headers = [
150 (h, headers[h])
150 (h, headers[h])
151 for h in headers
151 for h in headers
152 if h.lower() not in self.IGNORED_HEADERS
152 if h.lower() not in self.IGNORED_HEADERS
153 ]
153 ]
154
154
155 return headers
155 return headers
156
156
157
157
158 class DisabledSimpleSvnApp(object):
158 class DisabledSimpleSvnApp(object):
159 def __init__(self, config):
159 def __init__(self, config):
160 self.config = config
160 self.config = config
161
161
162 def __call__(self, environ, start_response):
162 def __call__(self, environ, start_response):
163 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
163 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
164 log.warning(reason)
164 log.warning(reason)
165 return HTTPNotAcceptable(reason)(environ, start_response)
165 return HTTPNotAcceptable(reason)(environ, start_response)
166
166
167
167
168 class SimpleSvn(simplevcs.SimpleVCS):
168 class SimpleSvn(simplevcs.SimpleVCS):
169
169
170 SCM = 'svn'
170 SCM = 'svn'
171 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
171 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
172 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
172 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
173
173
174 def _get_repository_name(self, environ):
174 def _get_repository_name(self, environ):
175 """
175 """
176 Gets repository name out of PATH_INFO header
176 Gets repository name out of PATH_INFO header
177
177
178 :param environ: environ where PATH_INFO is stored
178 :param environ: environ where PATH_INFO is stored
179 """
179 """
180 path = environ['PATH_INFO'].split('!')
180 path = environ['PATH_INFO'].split('!')
181 repo_name = path[0].strip('/')
181 repo_name = path[0].strip('/')
182
182
183 # SVN includes the whole path in it's requests, including
183 # SVN includes the whole path in it's requests, including
184 # subdirectories inside the repo. Therefore we have to search for
184 # subdirectories inside the repo. Therefore we have to search for
185 # the repo root directory.
185 # the repo root directory.
186 if not is_valid_repo(
186 if not is_valid_repo(
187 repo_name, self.base_path, explicit_scm=self.SCM):
187 repo_name, self.base_path, explicit_scm=self.SCM):
188 current_path = ''
188 current_path = ''
189 for component in repo_name.split('/'):
189 for component in repo_name.split('/'):
190 current_path += component
190 current_path += component
191 if is_valid_repo(
191 if is_valid_repo(
192 current_path, self.base_path, explicit_scm=self.SCM):
192 current_path, self.base_path, explicit_scm=self.SCM):
193 return current_path
193 return current_path
194 current_path += '/'
194 current_path += '/'
195
195
196 return repo_name
196 return repo_name
197
197
198 def _get_action(self, environ):
198 def _get_action(self, environ):
199 return (
199 return (
200 'pull'
200 'pull'
201 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
201 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
202 else 'push')
202 else 'push')
203
203
204 def _should_use_callback_daemon(self, extras, environ, action):
204 def _should_use_callback_daemon(self, extras, environ, action):
205 # only MERGE command triggers hooks, so we don't want to start
205 # only MERGE command triggers hooks, so we don't want to start
206 # hooks server too many times. POST however starts the svn transaction
206 # hooks server too many times. POST however starts the svn transaction
207 # so we also need to run the init of callback daemon of POST
207 # so we also need to run the init of callback daemon of POST
208 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
208 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
209 return True
209 return True
210 return False
210 return False
211
211
212 def _create_wsgi_app(self, repo_path, repo_name, config):
212 def _create_wsgi_app(self, repo_path, repo_name, config):
213 if self._is_svn_enabled():
213 if self._is_svn_enabled():
214 return SimpleSvnApp(config)
214 return SimpleSvnApp(config)
215 # we don't have http proxy enabled return dummy request handler
215 # we don't have http proxy enabled return dummy request handler
216 return DisabledSimpleSvnApp(config)
216 return DisabledSimpleSvnApp(config)
217
217
218 def _is_svn_enabled(self):
218 def _is_svn_enabled(self):
219 conf = self.repo_vcs_config
219 conf = self.repo_vcs_config
220 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
220 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
221
221
222 def _create_config(self, extras, repo_name):
222 def _create_config(self, extras, repo_name, scheme='http'):
223 conf = self.repo_vcs_config
223 conf = self.repo_vcs_config
224 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
224 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
225 server_url = server_url or self.DEFAULT_HTTP_SERVER
225 server_url = server_url or self.DEFAULT_HTTP_SERVER
226
226
227 extras['subversion_http_server_url'] = server_url
227 extras['subversion_http_server_url'] = server_url
228 return extras
228 return extras
@@ -1,669 +1,678 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import re
28 import logging
28 import logging
29 import importlib
29 import importlib
30 from functools import wraps
30 from functools import wraps
31 from StringIO import StringIO
31 from StringIO import StringIO
32 from lxml import etree
32 from lxml import etree
33
33
34 import time
34 import time
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36
36
37 from pyramid.httpexceptions import (
37 from pyramid.httpexceptions import (
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 from zope.cachedescriptors.property import Lazy as LazyProperty
39 from zope.cachedescriptors.property import Lazy as LazyProperty
40
40
41 import rhodecode
41 import rhodecode
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 from rhodecode.lib import rc_cache
43 from rhodecode.lib import rc_cache
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 from rhodecode.lib.base import (
45 from rhodecode.lib.base import (
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.middleware import appenlight
49 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware.utils import scm_app_http
50 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.backends import base
54 from rhodecode.lib.vcs.backends import base
55
55
56 from rhodecode.model import meta
56 from rhodecode.model import meta
57 from rhodecode.model.db import User, Repository, PullRequest
57 from rhodecode.model.db import User, Repository, PullRequest
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.pull_request import PullRequestModel
59 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 def extract_svn_txn_id(acl_repo_name, data):
65 def extract_svn_txn_id(acl_repo_name, data):
66 """
66 """
67 Helper method for extraction of svn txn_id from submitted XML data during
67 Helper method for extraction of svn txn_id from submitted XML data during
68 POST operations
68 POST operations
69 """
69 """
70 try:
70 try:
71 root = etree.fromstring(data)
71 root = etree.fromstring(data)
72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
73 for el in root:
73 for el in root:
74 if el.tag == '{DAV:}source':
74 if el.tag == '{DAV:}source':
75 for sub_el in el:
75 for sub_el in el:
76 if sub_el.tag == '{DAV:}href':
76 if sub_el.tag == '{DAV:}href':
77 match = pat.search(sub_el.text)
77 match = pat.search(sub_el.text)
78 if match:
78 if match:
79 svn_tx_id = match.groupdict()['txn_id']
79 svn_tx_id = match.groupdict()['txn_id']
80 txn_id = rc_cache.utils.compute_key_from_params(
80 txn_id = rc_cache.utils.compute_key_from_params(
81 acl_repo_name, svn_tx_id)
81 acl_repo_name, svn_tx_id)
82 return txn_id
82 return txn_id
83 except Exception:
83 except Exception:
84 log.exception('Failed to extract txn_id')
84 log.exception('Failed to extract txn_id')
85
85
86
86
87 def initialize_generator(factory):
87 def initialize_generator(factory):
88 """
88 """
89 Initializes the returned generator by draining its first element.
89 Initializes the returned generator by draining its first element.
90
90
91 This can be used to give a generator an initializer, which is the code
91 This can be used to give a generator an initializer, which is the code
92 up to the first yield statement. This decorator enforces that the first
92 up to the first yield statement. This decorator enforces that the first
93 produced element has the value ``"__init__"`` to make its special
93 produced element has the value ``"__init__"`` to make its special
94 purpose very explicit in the using code.
94 purpose very explicit in the using code.
95 """
95 """
96
96
97 @wraps(factory)
97 @wraps(factory)
98 def wrapper(*args, **kwargs):
98 def wrapper(*args, **kwargs):
99 gen = factory(*args, **kwargs)
99 gen = factory(*args, **kwargs)
100 try:
100 try:
101 init = gen.next()
101 init = gen.next()
102 except StopIteration:
102 except StopIteration:
103 raise ValueError('Generator must yield at least one element.')
103 raise ValueError('Generator must yield at least one element.')
104 if init != "__init__":
104 if init != "__init__":
105 raise ValueError('First yielded element must be "__init__".')
105 raise ValueError('First yielded element must be "__init__".')
106 return gen
106 return gen
107 return wrapper
107 return wrapper
108
108
109
109
110 class SimpleVCS(object):
110 class SimpleVCS(object):
111 """Common functionality for SCM HTTP handlers."""
111 """Common functionality for SCM HTTP handlers."""
112
112
113 SCM = 'unknown'
113 SCM = 'unknown'
114
114
115 acl_repo_name = None
115 acl_repo_name = None
116 url_repo_name = None
116 url_repo_name = None
117 vcs_repo_name = None
117 vcs_repo_name = None
118 rc_extras = {}
118 rc_extras = {}
119
119
120 # We have to handle requests to shadow repositories different than requests
120 # We have to handle requests to shadow repositories different than requests
121 # to normal repositories. Therefore we have to distinguish them. To do this
121 # to normal repositories. Therefore we have to distinguish them. To do this
122 # we use this regex which will match only on URLs pointing to shadow
122 # we use this regex which will match only on URLs pointing to shadow
123 # repositories.
123 # repositories.
124 shadow_repo_re = re.compile(
124 shadow_repo_re = re.compile(
125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
126 '(?P<target>{slug_pat})/' # target repo
126 '(?P<target>{slug_pat})/' # target repo
127 'pull-request/(?P<pr_id>\d+)/' # pull request
127 'pull-request/(?P<pr_id>\d+)/' # pull request
128 'repository$' # shadow repo
128 'repository$' # shadow repo
129 .format(slug_pat=SLUG_RE.pattern))
129 .format(slug_pat=SLUG_RE.pattern))
130
130
131 def __init__(self, config, registry):
131 def __init__(self, config, registry):
132 self.registry = registry
132 self.registry = registry
133 self.config = config
133 self.config = config
134 # re-populated by specialized middleware
134 # re-populated by specialized middleware
135 self.repo_vcs_config = base.Config()
135 self.repo_vcs_config = base.Config()
136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
137
137
138 registry.rhodecode_settings = self.rhodecode_settings
138 registry.rhodecode_settings = self.rhodecode_settings
139 # authenticate this VCS request using authfunc
139 # authenticate this VCS request using authfunc
140 auth_ret_code_detection = \
140 auth_ret_code_detection = \
141 str2bool(self.config.get('auth_ret_code_detection', False))
141 str2bool(self.config.get('auth_ret_code_detection', False))
142 self.authenticate = BasicAuth(
142 self.authenticate = BasicAuth(
143 '', authenticate, registry, config.get('auth_ret_code'),
143 '', authenticate, registry, config.get('auth_ret_code'),
144 auth_ret_code_detection)
144 auth_ret_code_detection)
145 self.ip_addr = '0.0.0.0'
145 self.ip_addr = '0.0.0.0'
146
146
147 @LazyProperty
147 @LazyProperty
148 def global_vcs_config(self):
148 def global_vcs_config(self):
149 try:
149 try:
150 return VcsSettingsModel().get_ui_settings_as_config_obj()
150 return VcsSettingsModel().get_ui_settings_as_config_obj()
151 except Exception:
151 except Exception:
152 return base.Config()
152 return base.Config()
153
153
154 @property
154 @property
155 def base_path(self):
155 def base_path(self):
156 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
156 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
157
157
158 if not settings_path:
158 if not settings_path:
159 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
159 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
160
160
161 if not settings_path:
161 if not settings_path:
162 # try, maybe we passed in explicitly as config option
162 # try, maybe we passed in explicitly as config option
163 settings_path = self.config.get('base_path')
163 settings_path = self.config.get('base_path')
164
164
165 if not settings_path:
165 if not settings_path:
166 raise ValueError('FATAL: base_path is empty')
166 raise ValueError('FATAL: base_path is empty')
167 return settings_path
167 return settings_path
168
168
169 def set_repo_names(self, environ):
169 def set_repo_names(self, environ):
170 """
170 """
171 This will populate the attributes acl_repo_name, url_repo_name,
171 This will populate the attributes acl_repo_name, url_repo_name,
172 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
172 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
173 shadow) repositories all names are equal. In case of requests to a
173 shadow) repositories all names are equal. In case of requests to a
174 shadow repository the acl-name points to the target repo of the pull
174 shadow repository the acl-name points to the target repo of the pull
175 request and the vcs-name points to the shadow repo file system path.
175 request and the vcs-name points to the shadow repo file system path.
176 The url-name is always the URL used by the vcs client program.
176 The url-name is always the URL used by the vcs client program.
177
177
178 Example in case of a shadow repo:
178 Example in case of a shadow repo:
179 acl_repo_name = RepoGroup/MyRepo
179 acl_repo_name = RepoGroup/MyRepo
180 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
180 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
181 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
181 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
182 """
182 """
183 # First we set the repo name from URL for all attributes. This is the
183 # First we set the repo name from URL for all attributes. This is the
184 # default if handling normal (non shadow) repo requests.
184 # default if handling normal (non shadow) repo requests.
185 self.url_repo_name = self._get_repository_name(environ)
185 self.url_repo_name = self._get_repository_name(environ)
186 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
186 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
187 self.is_shadow_repo = False
187 self.is_shadow_repo = False
188
188
189 # Check if this is a request to a shadow repository.
189 # Check if this is a request to a shadow repository.
190 match = self.shadow_repo_re.match(self.url_repo_name)
190 match = self.shadow_repo_re.match(self.url_repo_name)
191 if match:
191 if match:
192 match_dict = match.groupdict()
192 match_dict = match.groupdict()
193
193
194 # Build acl repo name from regex match.
194 # Build acl repo name from regex match.
195 acl_repo_name = safe_unicode('{groups}{target}'.format(
195 acl_repo_name = safe_unicode('{groups}{target}'.format(
196 groups=match_dict['groups'] or '',
196 groups=match_dict['groups'] or '',
197 target=match_dict['target']))
197 target=match_dict['target']))
198
198
199 # Retrieve pull request instance by ID from regex match.
199 # Retrieve pull request instance by ID from regex match.
200 pull_request = PullRequest.get(match_dict['pr_id'])
200 pull_request = PullRequest.get(match_dict['pr_id'])
201
201
202 # Only proceed if we got a pull request and if acl repo name from
202 # Only proceed if we got a pull request and if acl repo name from
203 # URL equals the target repo name of the pull request.
203 # URL equals the target repo name of the pull request.
204 if pull_request and \
204 if pull_request and \
205 (acl_repo_name == pull_request.target_repo.repo_name):
205 (acl_repo_name == pull_request.target_repo.repo_name):
206 repo_id = pull_request.target_repo.repo_id
206 repo_id = pull_request.target_repo.repo_id
207 # Get file system path to shadow repository.
207 # Get file system path to shadow repository.
208 workspace_id = PullRequestModel()._workspace_id(pull_request)
208 workspace_id = PullRequestModel()._workspace_id(pull_request)
209 target_vcs = pull_request.target_repo.scm_instance()
209 target_vcs = pull_request.target_repo.scm_instance()
210 vcs_repo_name = target_vcs._get_shadow_repository_path(
210 vcs_repo_name = target_vcs._get_shadow_repository_path(
211 repo_id, workspace_id)
211 repo_id, workspace_id)
212
212
213 # Store names for later usage.
213 # Store names for later usage.
214 self.vcs_repo_name = vcs_repo_name
214 self.vcs_repo_name = vcs_repo_name
215 self.acl_repo_name = acl_repo_name
215 self.acl_repo_name = acl_repo_name
216 self.is_shadow_repo = True
216 self.is_shadow_repo = True
217
217
218 log.debug('Setting all VCS repository names: %s', {
218 log.debug('Setting all VCS repository names: %s', {
219 'acl_repo_name': self.acl_repo_name,
219 'acl_repo_name': self.acl_repo_name,
220 'url_repo_name': self.url_repo_name,
220 'url_repo_name': self.url_repo_name,
221 'vcs_repo_name': self.vcs_repo_name,
221 'vcs_repo_name': self.vcs_repo_name,
222 })
222 })
223
223
224 @property
224 @property
225 def scm_app(self):
225 def scm_app(self):
226 custom_implementation = self.config['vcs.scm_app_implementation']
226 custom_implementation = self.config['vcs.scm_app_implementation']
227 if custom_implementation == 'http':
227 if custom_implementation == 'http':
228 log.info('Using HTTP implementation of scm app.')
228 log.info('Using HTTP implementation of scm app.')
229 scm_app_impl = scm_app_http
229 scm_app_impl = scm_app_http
230 else:
230 else:
231 log.info('Using custom implementation of scm_app: "{}"'.format(
231 log.info('Using custom implementation of scm_app: "{}"'.format(
232 custom_implementation))
232 custom_implementation))
233 scm_app_impl = importlib.import_module(custom_implementation)
233 scm_app_impl = importlib.import_module(custom_implementation)
234 return scm_app_impl
234 return scm_app_impl
235
235
236 def _get_by_id(self, repo_name):
236 def _get_by_id(self, repo_name):
237 """
237 """
238 Gets a special pattern _<ID> from clone url and tries to replace it
238 Gets a special pattern _<ID> from clone url and tries to replace it
239 with a repository_name for support of _<ID> non changeable urls
239 with a repository_name for support of _<ID> non changeable urls
240 """
240 """
241
241
242 data = repo_name.split('/')
242 data = repo_name.split('/')
243 if len(data) >= 2:
243 if len(data) >= 2:
244 from rhodecode.model.repo import RepoModel
244 from rhodecode.model.repo import RepoModel
245 by_id_match = RepoModel().get_repo_by_id(repo_name)
245 by_id_match = RepoModel().get_repo_by_id(repo_name)
246 if by_id_match:
246 if by_id_match:
247 data[1] = by_id_match.repo_name
247 data[1] = by_id_match.repo_name
248
248
249 return safe_str('/'.join(data))
249 return safe_str('/'.join(data))
250
250
251 def _invalidate_cache(self, repo_name):
251 def _invalidate_cache(self, repo_name):
252 """
252 """
253 Set's cache for this repository for invalidation on next access
253 Set's cache for this repository for invalidation on next access
254
254
255 :param repo_name: full repo name, also a cache key
255 :param repo_name: full repo name, also a cache key
256 """
256 """
257 ScmModel().mark_for_invalidation(repo_name)
257 ScmModel().mark_for_invalidation(repo_name)
258
258
259 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
259 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
260 db_repo = Repository.get_by_repo_name(repo_name)
260 db_repo = Repository.get_by_repo_name(repo_name)
261 if not db_repo:
261 if not db_repo:
262 log.debug('Repository `%s` not found inside the database.',
262 log.debug('Repository `%s` not found inside the database.',
263 repo_name)
263 repo_name)
264 return False
264 return False
265
265
266 if db_repo.repo_type != scm_type:
266 if db_repo.repo_type != scm_type:
267 log.warning(
267 log.warning(
268 'Repository `%s` have incorrect scm_type, expected %s got %s',
268 'Repository `%s` have incorrect scm_type, expected %s got %s',
269 repo_name, db_repo.repo_type, scm_type)
269 repo_name, db_repo.repo_type, scm_type)
270 return False
270 return False
271
271
272 config = db_repo._config
272 config = db_repo._config
273 config.set('extensions', 'largefiles', '')
273 config.set('extensions', 'largefiles', '')
274 return is_valid_repo(
274 return is_valid_repo(
275 repo_name, base_path,
275 repo_name, base_path,
276 explicit_scm=scm_type, expect_scm=scm_type, config=config)
276 explicit_scm=scm_type, expect_scm=scm_type, config=config)
277
277
278 def valid_and_active_user(self, user):
278 def valid_and_active_user(self, user):
279 """
279 """
280 Checks if that user is not empty, and if it's actually object it checks
280 Checks if that user is not empty, and if it's actually object it checks
281 if he's active.
281 if he's active.
282
282
283 :param user: user object or None
283 :param user: user object or None
284 :return: boolean
284 :return: boolean
285 """
285 """
286 if user is None:
286 if user is None:
287 return False
287 return False
288
288
289 elif user.active:
289 elif user.active:
290 return True
290 return True
291
291
292 return False
292 return False
293
293
294 @property
294 @property
295 def is_shadow_repo_dir(self):
295 def is_shadow_repo_dir(self):
296 return os.path.isdir(self.vcs_repo_name)
296 return os.path.isdir(self.vcs_repo_name)
297
297
298 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
298 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
299 plugin_id='', plugin_cache_active=False, cache_ttl=0):
299 plugin_id='', plugin_cache_active=False, cache_ttl=0):
300 """
300 """
301 Checks permissions using action (push/pull) user and repository
301 Checks permissions using action (push/pull) user and repository
302 name. If plugin_cache and ttl is set it will use the plugin which
302 name. If plugin_cache and ttl is set it will use the plugin which
303 authenticated the user to store the cached permissions result for N
303 authenticated the user to store the cached permissions result for N
304 amount of seconds as in cache_ttl
304 amount of seconds as in cache_ttl
305
305
306 :param action: push or pull action
306 :param action: push or pull action
307 :param user: user instance
307 :param user: user instance
308 :param repo_name: repository name
308 :param repo_name: repository name
309 """
309 """
310
310
311 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
311 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
312 plugin_id, plugin_cache_active, cache_ttl)
312 plugin_id, plugin_cache_active, cache_ttl)
313
313
314 user_id = user.user_id
314 user_id = user.user_id
315 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
315 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
316 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
316 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
317
317
318 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
318 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
319 expiration_time=cache_ttl,
319 expiration_time=cache_ttl,
320 condition=plugin_cache_active)
320 condition=plugin_cache_active)
321 def compute_perm_vcs(
321 def compute_perm_vcs(
322 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
322 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
323
323
324 log.debug('auth: calculating permission access now...')
324 log.debug('auth: calculating permission access now...')
325 # check IP
325 # check IP
326 inherit = user.inherit_default_permissions
326 inherit = user.inherit_default_permissions
327 ip_allowed = AuthUser.check_ip_allowed(
327 ip_allowed = AuthUser.check_ip_allowed(
328 user_id, ip_addr, inherit_from_default=inherit)
328 user_id, ip_addr, inherit_from_default=inherit)
329 if ip_allowed:
329 if ip_allowed:
330 log.info('Access for IP:%s allowed', ip_addr)
330 log.info('Access for IP:%s allowed', ip_addr)
331 else:
331 else:
332 return False
332 return False
333
333
334 if action == 'push':
334 if action == 'push':
335 perms = ('repository.write', 'repository.admin')
335 perms = ('repository.write', 'repository.admin')
336 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
336 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
337 return False
337 return False
338
338
339 else:
339 else:
340 # any other action need at least read permission
340 # any other action need at least read permission
341 perms = (
341 perms = (
342 'repository.read', 'repository.write', 'repository.admin')
342 'repository.read', 'repository.write', 'repository.admin')
343 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
343 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
344 return False
344 return False
345
345
346 return True
346 return True
347
347
348 start = time.time()
348 start = time.time()
349 log.debug('Running plugin `%s` permissions check', plugin_id)
349 log.debug('Running plugin `%s` permissions check', plugin_id)
350
350
351 # for environ based auth, password can be empty, but then the validation is
351 # for environ based auth, password can be empty, but then the validation is
352 # on the server that fills in the env data needed for authentication
352 # on the server that fills in the env data needed for authentication
353 perm_result = compute_perm_vcs(
353 perm_result = compute_perm_vcs(
354 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
354 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
355
355
356 auth_time = time.time() - start
356 auth_time = time.time() - start
357 log.debug('Permissions for plugin `%s` completed in %.3fs, '
357 log.debug('Permissions for plugin `%s` completed in %.3fs, '
358 'expiration time of fetched cache %.1fs.',
358 'expiration time of fetched cache %.1fs.',
359 plugin_id, auth_time, cache_ttl)
359 plugin_id, auth_time, cache_ttl)
360
360
361 return perm_result
361 return perm_result
362
362
363 def _get_http_scheme(self, environ):
364 try:
365 return environ['wsgi.url_scheme']
366 except Exception:
367 log.exception('Failed to read http scheme')
368 return 'http'
369
363 def _check_ssl(self, environ, start_response):
370 def _check_ssl(self, environ, start_response):
364 """
371 """
365 Checks the SSL check flag and returns False if SSL is not present
372 Checks the SSL check flag and returns False if SSL is not present
366 and required True otherwise
373 and required True otherwise
367 """
374 """
368 org_proto = environ['wsgi._org_proto']
375 org_proto = environ['wsgi._org_proto']
369 # check if we have SSL required ! if not it's a bad request !
376 # check if we have SSL required ! if not it's a bad request !
370 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
377 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
371 if require_ssl and org_proto == 'http':
378 if require_ssl and org_proto == 'http':
372 log.debug(
379 log.debug(
373 'Bad request: detected protocol is `%s` and '
380 'Bad request: detected protocol is `%s` and '
374 'SSL/HTTPS is required.', org_proto)
381 'SSL/HTTPS is required.', org_proto)
375 return False
382 return False
376 return True
383 return True
377
384
378 def _get_default_cache_ttl(self):
385 def _get_default_cache_ttl(self):
379 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
386 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
380 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
387 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
381 plugin_settings = plugin.get_settings()
388 plugin_settings = plugin.get_settings()
382 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
389 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
383 plugin_settings) or (False, 0)
390 plugin_settings) or (False, 0)
384 return plugin_cache_active, cache_ttl
391 return plugin_cache_active, cache_ttl
385
392
386 def __call__(self, environ, start_response):
393 def __call__(self, environ, start_response):
387 try:
394 try:
388 return self._handle_request(environ, start_response)
395 return self._handle_request(environ, start_response)
389 except Exception:
396 except Exception:
390 log.exception("Exception while handling request")
397 log.exception("Exception while handling request")
391 appenlight.track_exception(environ)
398 appenlight.track_exception(environ)
392 return HTTPInternalServerError()(environ, start_response)
399 return HTTPInternalServerError()(environ, start_response)
393 finally:
400 finally:
394 meta.Session.remove()
401 meta.Session.remove()
395
402
396 def _handle_request(self, environ, start_response):
403 def _handle_request(self, environ, start_response):
397 if not self._check_ssl(environ, start_response):
404 if not self._check_ssl(environ, start_response):
398 reason = ('SSL required, while RhodeCode was unable '
405 reason = ('SSL required, while RhodeCode was unable '
399 'to detect this as SSL request')
406 'to detect this as SSL request')
400 log.debug('User not allowed to proceed, %s', reason)
407 log.debug('User not allowed to proceed, %s', reason)
401 return HTTPNotAcceptable(reason)(environ, start_response)
408 return HTTPNotAcceptable(reason)(environ, start_response)
402
409
403 if not self.url_repo_name:
410 if not self.url_repo_name:
404 log.warning('Repository name is empty: %s', self.url_repo_name)
411 log.warning('Repository name is empty: %s', self.url_repo_name)
405 # failed to get repo name, we fail now
412 # failed to get repo name, we fail now
406 return HTTPNotFound()(environ, start_response)
413 return HTTPNotFound()(environ, start_response)
407 log.debug('Extracted repo name is %s', self.url_repo_name)
414 log.debug('Extracted repo name is %s', self.url_repo_name)
408
415
409 ip_addr = get_ip_addr(environ)
416 ip_addr = get_ip_addr(environ)
410 user_agent = get_user_agent(environ)
417 user_agent = get_user_agent(environ)
411 username = None
418 username = None
412
419
413 # skip passing error to error controller
420 # skip passing error to error controller
414 environ['pylons.status_code_redirect'] = True
421 environ['pylons.status_code_redirect'] = True
415
422
416 # ======================================================================
423 # ======================================================================
417 # GET ACTION PULL or PUSH
424 # GET ACTION PULL or PUSH
418 # ======================================================================
425 # ======================================================================
419 action = self._get_action(environ)
426 action = self._get_action(environ)
420
427
421 # ======================================================================
428 # ======================================================================
422 # Check if this is a request to a shadow repository of a pull request.
429 # Check if this is a request to a shadow repository of a pull request.
423 # In this case only pull action is allowed.
430 # In this case only pull action is allowed.
424 # ======================================================================
431 # ======================================================================
425 if self.is_shadow_repo and action != 'pull':
432 if self.is_shadow_repo and action != 'pull':
426 reason = 'Only pull action is allowed for shadow repositories.'
433 reason = 'Only pull action is allowed for shadow repositories.'
427 log.debug('User not allowed to proceed, %s', reason)
434 log.debug('User not allowed to proceed, %s', reason)
428 return HTTPNotAcceptable(reason)(environ, start_response)
435 return HTTPNotAcceptable(reason)(environ, start_response)
429
436
430 # Check if the shadow repo actually exists, in case someone refers
437 # Check if the shadow repo actually exists, in case someone refers
431 # to it, and it has been deleted because of successful merge.
438 # to it, and it has been deleted because of successful merge.
432 if self.is_shadow_repo and not self.is_shadow_repo_dir:
439 if self.is_shadow_repo and not self.is_shadow_repo_dir:
433 log.debug(
440 log.debug(
434 'Shadow repo detected, and shadow repo dir `%s` is missing',
441 'Shadow repo detected, and shadow repo dir `%s` is missing',
435 self.is_shadow_repo_dir)
442 self.is_shadow_repo_dir)
436 return HTTPNotFound()(environ, start_response)
443 return HTTPNotFound()(environ, start_response)
437
444
438 # ======================================================================
445 # ======================================================================
439 # CHECK ANONYMOUS PERMISSION
446 # CHECK ANONYMOUS PERMISSION
440 # ======================================================================
447 # ======================================================================
441 detect_force_push = False
448 detect_force_push = False
442 check_branch_perms = False
449 check_branch_perms = False
443 if action in ['pull', 'push']:
450 if action in ['pull', 'push']:
444 user_obj = anonymous_user = User.get_default_user()
451 user_obj = anonymous_user = User.get_default_user()
445 auth_user = user_obj.AuthUser()
452 auth_user = user_obj.AuthUser()
446 username = anonymous_user.username
453 username = anonymous_user.username
447 if anonymous_user.active:
454 if anonymous_user.active:
448 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
455 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
449 # ONLY check permissions if the user is activated
456 # ONLY check permissions if the user is activated
450 anonymous_perm = self._check_permission(
457 anonymous_perm = self._check_permission(
451 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
458 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
452 plugin_id='anonymous_access',
459 plugin_id='anonymous_access',
453 plugin_cache_active=plugin_cache_active,
460 plugin_cache_active=plugin_cache_active,
454 cache_ttl=cache_ttl,
461 cache_ttl=cache_ttl,
455 )
462 )
456 else:
463 else:
457 anonymous_perm = False
464 anonymous_perm = False
458
465
459 if not anonymous_user.active or not anonymous_perm:
466 if not anonymous_user.active or not anonymous_perm:
460 if not anonymous_user.active:
467 if not anonymous_user.active:
461 log.debug('Anonymous access is disabled, running '
468 log.debug('Anonymous access is disabled, running '
462 'authentication')
469 'authentication')
463
470
464 if not anonymous_perm:
471 if not anonymous_perm:
465 log.debug('Not enough credentials to access this '
472 log.debug('Not enough credentials to access this '
466 'repository as anonymous user')
473 'repository as anonymous user')
467
474
468 username = None
475 username = None
469 # ==============================================================
476 # ==============================================================
470 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
477 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
471 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
478 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
472 # ==============================================================
479 # ==============================================================
473
480
474 # try to auth based on environ, container auth methods
481 # try to auth based on environ, container auth methods
475 log.debug('Running PRE-AUTH for container based authentication')
482 log.debug('Running PRE-AUTH for container based authentication')
476 pre_auth = authenticate(
483 pre_auth = authenticate(
477 '', '', environ, VCS_TYPE, registry=self.registry,
484 '', '', environ, VCS_TYPE, registry=self.registry,
478 acl_repo_name=self.acl_repo_name)
485 acl_repo_name=self.acl_repo_name)
479 if pre_auth and pre_auth.get('username'):
486 if pre_auth and pre_auth.get('username'):
480 username = pre_auth['username']
487 username = pre_auth['username']
481 log.debug('PRE-AUTH got %s as username', username)
488 log.debug('PRE-AUTH got %s as username', username)
482 if pre_auth:
489 if pre_auth:
483 log.debug('PRE-AUTH successful from %s',
490 log.debug('PRE-AUTH successful from %s',
484 pre_auth.get('auth_data', {}).get('_plugin'))
491 pre_auth.get('auth_data', {}).get('_plugin'))
485
492
486 # If not authenticated by the container, running basic auth
493 # If not authenticated by the container, running basic auth
487 # before inject the calling repo_name for special scope checks
494 # before inject the calling repo_name for special scope checks
488 self.authenticate.acl_repo_name = self.acl_repo_name
495 self.authenticate.acl_repo_name = self.acl_repo_name
489
496
490 plugin_cache_active, cache_ttl = False, 0
497 plugin_cache_active, cache_ttl = False, 0
491 plugin = None
498 plugin = None
492 if not username:
499 if not username:
493 self.authenticate.realm = self.authenticate.get_rc_realm()
500 self.authenticate.realm = self.authenticate.get_rc_realm()
494
501
495 try:
502 try:
496 auth_result = self.authenticate(environ)
503 auth_result = self.authenticate(environ)
497 except (UserCreationError, NotAllowedToCreateUserError) as e:
504 except (UserCreationError, NotAllowedToCreateUserError) as e:
498 log.error(e)
505 log.error(e)
499 reason = safe_str(e)
506 reason = safe_str(e)
500 return HTTPNotAcceptable(reason)(environ, start_response)
507 return HTTPNotAcceptable(reason)(environ, start_response)
501
508
502 if isinstance(auth_result, dict):
509 if isinstance(auth_result, dict):
503 AUTH_TYPE.update(environ, 'basic')
510 AUTH_TYPE.update(environ, 'basic')
504 REMOTE_USER.update(environ, auth_result['username'])
511 REMOTE_USER.update(environ, auth_result['username'])
505 username = auth_result['username']
512 username = auth_result['username']
506 plugin = auth_result.get('auth_data', {}).get('_plugin')
513 plugin = auth_result.get('auth_data', {}).get('_plugin')
507 log.info(
514 log.info(
508 'MAIN-AUTH successful for user `%s` from %s plugin',
515 'MAIN-AUTH successful for user `%s` from %s plugin',
509 username, plugin)
516 username, plugin)
510
517
511 plugin_cache_active, cache_ttl = auth_result.get(
518 plugin_cache_active, cache_ttl = auth_result.get(
512 'auth_data', {}).get('_ttl_cache') or (False, 0)
519 'auth_data', {}).get('_ttl_cache') or (False, 0)
513 else:
520 else:
514 return auth_result.wsgi_application(environ, start_response)
521 return auth_result.wsgi_application(environ, start_response)
515
522
516 # ==============================================================
523 # ==============================================================
517 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
524 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
518 # ==============================================================
525 # ==============================================================
519 user = User.get_by_username(username)
526 user = User.get_by_username(username)
520 if not self.valid_and_active_user(user):
527 if not self.valid_and_active_user(user):
521 return HTTPForbidden()(environ, start_response)
528 return HTTPForbidden()(environ, start_response)
522 username = user.username
529 username = user.username
523 user_id = user.user_id
530 user_id = user.user_id
524
531
525 # check user attributes for password change flag
532 # check user attributes for password change flag
526 user_obj = user
533 user_obj = user
527 auth_user = user_obj.AuthUser()
534 auth_user = user_obj.AuthUser()
528 if user_obj and user_obj.username != User.DEFAULT_USER and \
535 if user_obj and user_obj.username != User.DEFAULT_USER and \
529 user_obj.user_data.get('force_password_change'):
536 user_obj.user_data.get('force_password_change'):
530 reason = 'password change required'
537 reason = 'password change required'
531 log.debug('User not allowed to authenticate, %s', reason)
538 log.debug('User not allowed to authenticate, %s', reason)
532 return HTTPNotAcceptable(reason)(environ, start_response)
539 return HTTPNotAcceptable(reason)(environ, start_response)
533
540
534 # check permissions for this repository
541 # check permissions for this repository
535 perm = self._check_permission(
542 perm = self._check_permission(
536 action, user, auth_user, self.acl_repo_name, ip_addr,
543 action, user, auth_user, self.acl_repo_name, ip_addr,
537 plugin, plugin_cache_active, cache_ttl)
544 plugin, plugin_cache_active, cache_ttl)
538 if not perm:
545 if not perm:
539 return HTTPForbidden()(environ, start_response)
546 return HTTPForbidden()(environ, start_response)
540 environ['rc_auth_user_id'] = user_id
547 environ['rc_auth_user_id'] = user_id
541
548
542 if action == 'push':
549 if action == 'push':
543 perms = auth_user.get_branch_permissions(self.acl_repo_name)
550 perms = auth_user.get_branch_permissions(self.acl_repo_name)
544 if perms:
551 if perms:
545 check_branch_perms = True
552 check_branch_perms = True
546 detect_force_push = True
553 detect_force_push = True
547
554
548 # extras are injected into UI object and later available
555 # extras are injected into UI object and later available
549 # in hooks executed by RhodeCode
556 # in hooks executed by RhodeCode
550 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
557 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
551
558
552 extras = vcs_operation_context(
559 extras = vcs_operation_context(
553 environ, repo_name=self.acl_repo_name, username=username,
560 environ, repo_name=self.acl_repo_name, username=username,
554 action=action, scm=self.SCM, check_locking=check_locking,
561 action=action, scm=self.SCM, check_locking=check_locking,
555 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
562 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
556 detect_force_push=detect_force_push
563 detect_force_push=detect_force_push
557 )
564 )
558
565
559 # ======================================================================
566 # ======================================================================
560 # REQUEST HANDLING
567 # REQUEST HANDLING
561 # ======================================================================
568 # ======================================================================
562 repo_path = os.path.join(
569 repo_path = os.path.join(
563 safe_str(self.base_path), safe_str(self.vcs_repo_name))
570 safe_str(self.base_path), safe_str(self.vcs_repo_name))
564 log.debug('Repository path is %s', repo_path)
571 log.debug('Repository path is %s', repo_path)
565
572
566 fix_PATH()
573 fix_PATH()
567
574
568 log.info(
575 log.info(
569 '%s action on %s repo "%s" by "%s" from %s %s',
576 '%s action on %s repo "%s" by "%s" from %s %s',
570 action, self.SCM, safe_str(self.url_repo_name),
577 action, self.SCM, safe_str(self.url_repo_name),
571 safe_str(username), ip_addr, user_agent)
578 safe_str(username), ip_addr, user_agent)
572
579
573 return self._generate_vcs_response(
580 return self._generate_vcs_response(
574 environ, start_response, repo_path, extras, action)
581 environ, start_response, repo_path, extras, action)
575
582
576 @initialize_generator
583 @initialize_generator
577 def _generate_vcs_response(
584 def _generate_vcs_response(
578 self, environ, start_response, repo_path, extras, action):
585 self, environ, start_response, repo_path, extras, action):
579 """
586 """
580 Returns a generator for the response content.
587 Returns a generator for the response content.
581
588
582 This method is implemented as a generator, so that it can trigger
589 This method is implemented as a generator, so that it can trigger
583 the cache validation after all content sent back to the client. It
590 the cache validation after all content sent back to the client. It
584 also handles the locking exceptions which will be triggered when
591 also handles the locking exceptions which will be triggered when
585 the first chunk is produced by the underlying WSGI application.
592 the first chunk is produced by the underlying WSGI application.
586 """
593 """
587 txn_id = ''
594 txn_id = ''
588 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
595 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
589 # case for SVN, we want to re-use the callback daemon port
596 # case for SVN, we want to re-use the callback daemon port
590 # so we use the txn_id, for this we peek the body, and still save
597 # so we use the txn_id, for this we peek the body, and still save
591 # it as wsgi.input
598 # it as wsgi.input
592 data = environ['wsgi.input'].read()
599 data = environ['wsgi.input'].read()
593 environ['wsgi.input'] = StringIO(data)
600 environ['wsgi.input'] = StringIO(data)
594 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
601 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
595
602
596 callback_daemon, extras = self._prepare_callback_daemon(
603 callback_daemon, extras = self._prepare_callback_daemon(
597 extras, environ, action, txn_id=txn_id)
604 extras, environ, action, txn_id=txn_id)
598 log.debug('HOOKS extras is %s', extras)
605 log.debug('HOOKS extras is %s', extras)
599
606
600 config = self._create_config(extras, self.acl_repo_name)
607 http_scheme = self._get_http_scheme(environ)
608
609 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
601 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
610 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
602 with callback_daemon:
611 with callback_daemon:
603 app.rc_extras = extras
612 app.rc_extras = extras
604
613
605 try:
614 try:
606 response = app(environ, start_response)
615 response = app(environ, start_response)
607 finally:
616 finally:
608 # This statement works together with the decorator
617 # This statement works together with the decorator
609 # "initialize_generator" above. The decorator ensures that
618 # "initialize_generator" above. The decorator ensures that
610 # we hit the first yield statement before the generator is
619 # we hit the first yield statement before the generator is
611 # returned back to the WSGI server. This is needed to
620 # returned back to the WSGI server. This is needed to
612 # ensure that the call to "app" above triggers the
621 # ensure that the call to "app" above triggers the
613 # needed callback to "start_response" before the
622 # needed callback to "start_response" before the
614 # generator is actually used.
623 # generator is actually used.
615 yield "__init__"
624 yield "__init__"
616
625
617 # iter content
626 # iter content
618 for chunk in response:
627 for chunk in response:
619 yield chunk
628 yield chunk
620
629
621 try:
630 try:
622 # invalidate cache on push
631 # invalidate cache on push
623 if action == 'push':
632 if action == 'push':
624 self._invalidate_cache(self.url_repo_name)
633 self._invalidate_cache(self.url_repo_name)
625 finally:
634 finally:
626 meta.Session.remove()
635 meta.Session.remove()
627
636
628 def _get_repository_name(self, environ):
637 def _get_repository_name(self, environ):
629 """Get repository name out of the environmnent
638 """Get repository name out of the environmnent
630
639
631 :param environ: WSGI environment
640 :param environ: WSGI environment
632 """
641 """
633 raise NotImplementedError()
642 raise NotImplementedError()
634
643
635 def _get_action(self, environ):
644 def _get_action(self, environ):
636 """Map request commands into a pull or push command.
645 """Map request commands into a pull or push command.
637
646
638 :param environ: WSGI environment
647 :param environ: WSGI environment
639 """
648 """
640 raise NotImplementedError()
649 raise NotImplementedError()
641
650
642 def _create_wsgi_app(self, repo_path, repo_name, config):
651 def _create_wsgi_app(self, repo_path, repo_name, config):
643 """Return the WSGI app that will finally handle the request."""
652 """Return the WSGI app that will finally handle the request."""
644 raise NotImplementedError()
653 raise NotImplementedError()
645
654
646 def _create_config(self, extras, repo_name):
655 def _create_config(self, extras, repo_name, scheme='http'):
647 """Create a safe config representation."""
656 """Create a safe config representation."""
648 raise NotImplementedError()
657 raise NotImplementedError()
649
658
650 def _should_use_callback_daemon(self, extras, environ, action):
659 def _should_use_callback_daemon(self, extras, environ, action):
651 return True
660 return True
652
661
653 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
662 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
654 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
663 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
655 if not self._should_use_callback_daemon(extras, environ, action):
664 if not self._should_use_callback_daemon(extras, environ, action):
656 # disable callback daemon for actions that don't require it
665 # disable callback daemon for actions that don't require it
657 direct_calls = True
666 direct_calls = True
658
667
659 return prepare_callback_daemon(
668 return prepare_callback_daemon(
660 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
669 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
661 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
670 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
662
671
663
672
664 def _should_check_locking(query_string):
673 def _should_check_locking(query_string):
665 # this is kind of hacky, but due to how mercurial handles client-server
674 # this is kind of hacky, but due to how mercurial handles client-server
666 # server see all operation on commit; bookmarks, phases and
675 # server see all operation on commit; bookmarks, phases and
667 # obsolescence marker in different transaction, we don't want to check
676 # obsolescence marker in different transaction, we don't want to check
668 # locking on those
677 # locking on those
669 return query_string not in ['cmd=listkeys']
678 return query_string not in ['cmd=listkeys']
@@ -1,139 +1,140 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22 import urlparse
22 import urlparse
23 import mock
23 import mock
24 import simplejson as json
24 import simplejson as json
25
25
26 from rhodecode.lib.vcs.backends.base import Config
26 from rhodecode.lib.vcs.backends.base import Config
27 from rhodecode.tests.lib.middleware import mock_scm_app
27 from rhodecode.tests.lib.middleware import mock_scm_app
28 import rhodecode.lib.middleware.simplegit as simplegit
28 import rhodecode.lib.middleware.simplegit as simplegit
29
29
30
30
31 def get_environ(url, request_method):
31 def get_environ(url, request_method):
32 """Construct a minimum WSGI environ based on the URL."""
32 """Construct a minimum WSGI environ based on the URL."""
33 parsed_url = urlparse.urlparse(url)
33 parsed_url = urlparse.urlparse(url)
34 environ = {
34 environ = {
35 'PATH_INFO': parsed_url.path,
35 'PATH_INFO': parsed_url.path,
36 'QUERY_STRING': parsed_url.query,
36 'QUERY_STRING': parsed_url.query,
37 'REQUEST_METHOD': request_method,
37 'REQUEST_METHOD': request_method,
38 }
38 }
39
39
40 return environ
40 return environ
41
41
42
42
43 @pytest.mark.parametrize(
43 @pytest.mark.parametrize(
44 'url, expected_action, request_method',
44 'url, expected_action, request_method',
45 [
45 [
46 ('/foo/bar/info/refs?service=git-upload-pack', 'pull', 'GET'),
46 ('/foo/bar/info/refs?service=git-upload-pack', 'pull', 'GET'),
47 ('/foo/bar/info/refs?service=git-receive-pack', 'push', 'GET'),
47 ('/foo/bar/info/refs?service=git-receive-pack', 'push', 'GET'),
48 ('/foo/bar/git-upload-pack', 'pull', 'GET'),
48 ('/foo/bar/git-upload-pack', 'pull', 'GET'),
49 ('/foo/bar/git-receive-pack', 'push', 'GET'),
49 ('/foo/bar/git-receive-pack', 'push', 'GET'),
50 # Edge case: missing data for info/refs
50 # Edge case: missing data for info/refs
51 ('/foo/info/refs?service=', 'pull', 'GET'),
51 ('/foo/info/refs?service=', 'pull', 'GET'),
52 ('/foo/info/refs', 'pull', 'GET'),
52 ('/foo/info/refs', 'pull', 'GET'),
53 # Edge case: git command comes with service argument
53 # Edge case: git command comes with service argument
54 ('/foo/git-upload-pack?service=git-receive-pack', 'pull', 'GET'),
54 ('/foo/git-upload-pack?service=git-receive-pack', 'pull', 'GET'),
55 ('/foo/git-receive-pack?service=git-upload-pack', 'push', 'GET'),
55 ('/foo/git-receive-pack?service=git-upload-pack', 'push', 'GET'),
56 # Edge case: repo name conflicts with git commands
56 # Edge case: repo name conflicts with git commands
57 ('/git-receive-pack/git-upload-pack', 'pull', 'GET'),
57 ('/git-receive-pack/git-upload-pack', 'pull', 'GET'),
58 ('/git-receive-pack/git-receive-pack', 'push', 'GET'),
58 ('/git-receive-pack/git-receive-pack', 'push', 'GET'),
59 ('/git-upload-pack/git-upload-pack', 'pull', 'GET'),
59 ('/git-upload-pack/git-upload-pack', 'pull', 'GET'),
60 ('/git-upload-pack/git-receive-pack', 'push', 'GET'),
60 ('/git-upload-pack/git-receive-pack', 'push', 'GET'),
61 ('/foo/git-receive-pack', 'push', 'GET'),
61 ('/foo/git-receive-pack', 'push', 'GET'),
62 # Edge case: not a smart protocol url
62 # Edge case: not a smart protocol url
63 ('/foo/bar', 'pull', 'GET'),
63 ('/foo/bar', 'pull', 'GET'),
64 # GIT LFS cases, batch
64 # GIT LFS cases, batch
65 ('/foo/bar/info/lfs/objects/batch', 'push', 'GET'),
65 ('/foo/bar/info/lfs/objects/batch', 'push', 'GET'),
66 ('/foo/bar/info/lfs/objects/batch', 'pull', 'POST'),
66 ('/foo/bar/info/lfs/objects/batch', 'pull', 'POST'),
67 # GIT LFS oid, dl/upl
67 # GIT LFS oid, dl/upl
68 ('/foo/bar/info/lfs/abcdeabcde', 'pull', 'GET'),
68 ('/foo/bar/info/lfs/abcdeabcde', 'pull', 'GET'),
69 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'PUT'),
69 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'PUT'),
70 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'POST'),
70 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'POST'),
71 # Edge case: repo name conflicts with git commands
71 # Edge case: repo name conflicts with git commands
72 ('/info/lfs/info/lfs/objects/batch', 'push', 'GET'),
72 ('/info/lfs/info/lfs/objects/batch', 'push', 'GET'),
73 ('/info/lfs/info/lfs/objects/batch', 'pull', 'POST'),
73 ('/info/lfs/info/lfs/objects/batch', 'pull', 'POST'),
74
74
75 ])
75 ])
76 def test_get_action(url, expected_action, request_method, baseapp, request_stub):
76 def test_get_action(url, expected_action, request_method, baseapp, request_stub):
77 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
77 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
78 registry=request_stub.registry)
78 registry=request_stub.registry)
79 assert expected_action == app._get_action(get_environ(url, request_method))
79 assert expected_action == app._get_action(get_environ(url, request_method))
80
80
81
81
82 @pytest.mark.parametrize(
82 @pytest.mark.parametrize(
83 'url, expected_repo_name, request_method',
83 'url, expected_repo_name, request_method',
84 [
84 [
85 ('/foo/info/refs?service=git-upload-pack', 'foo', 'GET'),
85 ('/foo/info/refs?service=git-upload-pack', 'foo', 'GET'),
86 ('/foo/bar/info/refs?service=git-receive-pack', 'foo/bar', 'GET'),
86 ('/foo/bar/info/refs?service=git-receive-pack', 'foo/bar', 'GET'),
87 ('/foo/git-upload-pack', 'foo', 'GET'),
87 ('/foo/git-upload-pack', 'foo', 'GET'),
88 ('/foo/git-receive-pack', 'foo', 'GET'),
88 ('/foo/git-receive-pack', 'foo', 'GET'),
89 ('/foo/bar/git-upload-pack', 'foo/bar', 'GET'),
89 ('/foo/bar/git-upload-pack', 'foo/bar', 'GET'),
90 ('/foo/bar/git-receive-pack', 'foo/bar', 'GET'),
90 ('/foo/bar/git-receive-pack', 'foo/bar', 'GET'),
91
91
92 # GIT LFS cases, batch
92 # GIT LFS cases, batch
93 ('/foo/bar/info/lfs/objects/batch', 'foo/bar', 'GET'),
93 ('/foo/bar/info/lfs/objects/batch', 'foo/bar', 'GET'),
94 ('/example-git/info/lfs/objects/batch', 'example-git', 'POST'),
94 ('/example-git/info/lfs/objects/batch', 'example-git', 'POST'),
95 # GIT LFS oid, dl/upl
95 # GIT LFS oid, dl/upl
96 ('/foo/info/lfs/abcdeabcde', 'foo', 'GET'),
96 ('/foo/info/lfs/abcdeabcde', 'foo', 'GET'),
97 ('/foo/bar/info/lfs/abcdeabcde', 'foo/bar', 'PUT'),
97 ('/foo/bar/info/lfs/abcdeabcde', 'foo/bar', 'PUT'),
98 ('/my-git-repo/info/lfs/abcdeabcde', 'my-git-repo', 'POST'),
98 ('/my-git-repo/info/lfs/abcdeabcde', 'my-git-repo', 'POST'),
99 # Edge case: repo name conflicts with git commands
99 # Edge case: repo name conflicts with git commands
100 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'GET'),
100 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'GET'),
101 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'POST'),
101 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'POST'),
102
102
103 ])
103 ])
104 def test_get_repository_name(url, expected_repo_name, request_method, baseapp, request_stub):
104 def test_get_repository_name(url, expected_repo_name, request_method, baseapp, request_stub):
105 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
105 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
106 registry=request_stub.registry)
106 registry=request_stub.registry)
107 assert expected_repo_name == app._get_repository_name(
107 assert expected_repo_name == app._get_repository_name(
108 get_environ(url, request_method))
108 get_environ(url, request_method))
109
109
110
110
111 def test_get_config(user_util, baseapp, request_stub):
111 def test_get_config(user_util, baseapp, request_stub):
112 repo = user_util.create_repo(repo_type='git')
112 repo = user_util.create_repo(repo_type='git')
113 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
113 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
114 registry=request_stub.registry)
114 registry=request_stub.registry)
115 extras = {'foo': 'FOO', 'bar': 'BAR'}
115 extras = {'foo': 'FOO', 'bar': 'BAR'}
116
116
117 # We copy the extras as the method below will change the contents.
117 # We copy the extras as the method below will change the contents.
118 git_config = app._create_config(dict(extras), repo_name=repo.repo_name)
118 git_config = app._create_config(dict(extras), repo_name=repo.repo_name)
119
119
120 expected_config = dict(extras)
120 expected_config = dict(extras)
121 expected_config.update({
121 expected_config.update({
122 'git_update_server_info': False,
122 'git_update_server_info': False,
123 'git_lfs_enabled': False,
123 'git_lfs_enabled': False,
124 'git_lfs_store_path': git_config['git_lfs_store_path']
124 'git_lfs_store_path': git_config['git_lfs_store_path'],
125 'git_lfs_http_scheme': 'http'
125 })
126 })
126
127
127 assert git_config == expected_config
128 assert git_config == expected_config
128
129
129
130
130 def test_create_wsgi_app_uses_scm_app_from_simplevcs(baseapp, request_stub):
131 def test_create_wsgi_app_uses_scm_app_from_simplevcs(baseapp, request_stub):
131 config = {
132 config = {
132 'auth_ret_code': '',
133 'auth_ret_code': '',
133 'base_path': '',
134 'base_path': '',
134 'vcs.scm_app_implementation':
135 'vcs.scm_app_implementation':
135 'rhodecode.tests.lib.middleware.mock_scm_app',
136 'rhodecode.tests.lib.middleware.mock_scm_app',
136 }
137 }
137 app = simplegit.SimpleGit(config=config, registry=request_stub.registry)
138 app = simplegit.SimpleGit(config=config, registry=request_stub.registry)
138 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
139 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
139 assert wsgi_app is mock_scm_app.mock_git_wsgi
140 assert wsgi_app is mock_scm_app.mock_git_wsgi
@@ -1,475 +1,475 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import base64
21 import base64
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.lib.utils2 import AttributeDict
27 from rhodecode.tests.utils import CustomTestApp
27 from rhodecode.tests.utils import CustomTestApp
28
28
29 from rhodecode.lib.caching_query import FromCache
29 from rhodecode.lib.caching_query import FromCache
30 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
30 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
31 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.middleware import simplevcs
32 from rhodecode.lib.middleware.https_fixup import HttpsFixup
32 from rhodecode.lib.middleware.https_fixup import HttpsFixup
33 from rhodecode.lib.middleware.utils import scm_app_http
33 from rhodecode.lib.middleware.utils import scm_app_http
34 from rhodecode.model.db import User, _hash_key
34 from rhodecode.model.db import User, _hash_key
35 from rhodecode.model.meta import Session
35 from rhodecode.model.meta import Session
36 from rhodecode.tests import (
36 from rhodecode.tests import (
37 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
37 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
38 from rhodecode.tests.lib.middleware import mock_scm_app
38 from rhodecode.tests.lib.middleware import mock_scm_app
39
39
40
40
41 class StubVCSController(simplevcs.SimpleVCS):
41 class StubVCSController(simplevcs.SimpleVCS):
42
42
43 SCM = 'hg'
43 SCM = 'hg'
44 stub_response_body = tuple()
44 stub_response_body = tuple()
45
45
46 def __init__(self, *args, **kwargs):
46 def __init__(self, *args, **kwargs):
47 super(StubVCSController, self).__init__(*args, **kwargs)
47 super(StubVCSController, self).__init__(*args, **kwargs)
48 self._action = 'pull'
48 self._action = 'pull'
49 self._is_shadow_repo_dir = True
49 self._is_shadow_repo_dir = True
50 self._name = HG_REPO
50 self._name = HG_REPO
51 self.set_repo_names(None)
51 self.set_repo_names(None)
52
52
53 @property
53 @property
54 def is_shadow_repo_dir(self):
54 def is_shadow_repo_dir(self):
55 return self._is_shadow_repo_dir
55 return self._is_shadow_repo_dir
56
56
57 def _get_repository_name(self, environ):
57 def _get_repository_name(self, environ):
58 return self._name
58 return self._name
59
59
60 def _get_action(self, environ):
60 def _get_action(self, environ):
61 return self._action
61 return self._action
62
62
63 def _create_wsgi_app(self, repo_path, repo_name, config):
63 def _create_wsgi_app(self, repo_path, repo_name, config):
64 def fake_app(environ, start_response):
64 def fake_app(environ, start_response):
65 headers = [
65 headers = [
66 ('Http-Accept', 'application/mercurial')
66 ('Http-Accept', 'application/mercurial')
67 ]
67 ]
68 start_response('200 OK', headers)
68 start_response('200 OK', headers)
69 return self.stub_response_body
69 return self.stub_response_body
70 return fake_app
70 return fake_app
71
71
72 def _create_config(self, extras, repo_name):
72 def _create_config(self, extras, repo_name, scheme='http'):
73 return None
73 return None
74
74
75
75
76 @pytest.fixture
76 @pytest.fixture
77 def vcscontroller(baseapp, config_stub, request_stub):
77 def vcscontroller(baseapp, config_stub, request_stub):
78 config_stub.testing_securitypolicy()
78 config_stub.testing_securitypolicy()
79 config_stub.include('rhodecode.authentication')
79 config_stub.include('rhodecode.authentication')
80 config_stub.include('rhodecode.authentication.plugins.auth_rhodecode')
80 config_stub.include('rhodecode.authentication.plugins.auth_rhodecode')
81 config_stub.include('rhodecode.authentication.plugins.auth_token')
81 config_stub.include('rhodecode.authentication.plugins.auth_token')
82
82
83 controller = StubVCSController(
83 controller = StubVCSController(
84 baseapp.config.get_settings(), request_stub.registry)
84 baseapp.config.get_settings(), request_stub.registry)
85 app = HttpsFixup(controller, baseapp.config.get_settings())
85 app = HttpsFixup(controller, baseapp.config.get_settings())
86 app = CustomTestApp(app)
86 app = CustomTestApp(app)
87
87
88 _remove_default_user_from_query_cache()
88 _remove_default_user_from_query_cache()
89
89
90 # Sanity checks that things are set up correctly
90 # Sanity checks that things are set up correctly
91 app.get('/' + HG_REPO, status=200)
91 app.get('/' + HG_REPO, status=200)
92
92
93 app.controller = controller
93 app.controller = controller
94 return app
94 return app
95
95
96
96
97 def _remove_default_user_from_query_cache():
97 def _remove_default_user_from_query_cache():
98 user = User.get_default_user(cache=True)
98 user = User.get_default_user(cache=True)
99 query = Session().query(User).filter(User.username == user.username)
99 query = Session().query(User).filter(User.username == user.username)
100 query = query.options(
100 query = query.options(
101 FromCache("sql_cache_short", "get_user_%s" % _hash_key(user.username)))
101 FromCache("sql_cache_short", "get_user_%s" % _hash_key(user.username)))
102 query.invalidate()
102 query.invalidate()
103 Session().expire(user)
103 Session().expire(user)
104
104
105
105
106 def test_handles_exceptions_during_permissions_checks(
106 def test_handles_exceptions_during_permissions_checks(
107 vcscontroller, disable_anonymous_user):
107 vcscontroller, disable_anonymous_user):
108 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
108 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
109 auth_password = base64.encodestring(user_and_pass).strip()
109 auth_password = base64.encodestring(user_and_pass).strip()
110 extra_environ = {
110 extra_environ = {
111 'AUTH_TYPE': 'Basic',
111 'AUTH_TYPE': 'Basic',
112 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
112 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
113 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
113 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
114 }
114 }
115
115
116 # Verify that things are hooked up correctly
116 # Verify that things are hooked up correctly
117 vcscontroller.get('/', status=200, extra_environ=extra_environ)
117 vcscontroller.get('/', status=200, extra_environ=extra_environ)
118
118
119 # Simulate trouble during permission checks
119 # Simulate trouble during permission checks
120 with mock.patch('rhodecode.model.db.User.get_by_username',
120 with mock.patch('rhodecode.model.db.User.get_by_username',
121 side_effect=Exception) as get_user:
121 side_effect=Exception) as get_user:
122 # Verify that a correct 500 is returned and check that the expected
122 # Verify that a correct 500 is returned and check that the expected
123 # code path was hit.
123 # code path was hit.
124 vcscontroller.get('/', status=500, extra_environ=extra_environ)
124 vcscontroller.get('/', status=500, extra_environ=extra_environ)
125 assert get_user.called
125 assert get_user.called
126
126
127
127
128 def test_returns_forbidden_if_no_anonymous_access(
128 def test_returns_forbidden_if_no_anonymous_access(
129 vcscontroller, disable_anonymous_user):
129 vcscontroller, disable_anonymous_user):
130 vcscontroller.get('/', status=401)
130 vcscontroller.get('/', status=401)
131
131
132
132
133 class StubFailVCSController(simplevcs.SimpleVCS):
133 class StubFailVCSController(simplevcs.SimpleVCS):
134 def _handle_request(self, environ, start_response):
134 def _handle_request(self, environ, start_response):
135 raise Exception("BOOM")
135 raise Exception("BOOM")
136
136
137
137
138 @pytest.fixture(scope='module')
138 @pytest.fixture(scope='module')
139 def fail_controller(baseapp):
139 def fail_controller(baseapp):
140 controller = StubFailVCSController(
140 controller = StubFailVCSController(
141 baseapp.config.get_settings(), baseapp.config)
141 baseapp.config.get_settings(), baseapp.config)
142 controller = HttpsFixup(controller, baseapp.config.get_settings())
142 controller = HttpsFixup(controller, baseapp.config.get_settings())
143 controller = CustomTestApp(controller)
143 controller = CustomTestApp(controller)
144 return controller
144 return controller
145
145
146
146
147 def test_handles_exceptions_as_internal_server_error(fail_controller):
147 def test_handles_exceptions_as_internal_server_error(fail_controller):
148 fail_controller.get('/', status=500)
148 fail_controller.get('/', status=500)
149
149
150
150
151 def test_provides_traceback_for_appenlight(fail_controller):
151 def test_provides_traceback_for_appenlight(fail_controller):
152 response = fail_controller.get(
152 response = fail_controller.get(
153 '/', status=500, extra_environ={'appenlight.client': 'fake'})
153 '/', status=500, extra_environ={'appenlight.client': 'fake'})
154 assert 'appenlight.__traceback' in response.request.environ
154 assert 'appenlight.__traceback' in response.request.environ
155
155
156
156
157 def test_provides_utils_scm_app_as_scm_app_by_default(baseapp, request_stub):
157 def test_provides_utils_scm_app_as_scm_app_by_default(baseapp, request_stub):
158 controller = StubVCSController(baseapp.config.get_settings(), request_stub.registry)
158 controller = StubVCSController(baseapp.config.get_settings(), request_stub.registry)
159 assert controller.scm_app is scm_app_http
159 assert controller.scm_app is scm_app_http
160
160
161
161
162 def test_allows_to_override_scm_app_via_config(baseapp, request_stub):
162 def test_allows_to_override_scm_app_via_config(baseapp, request_stub):
163 config = baseapp.config.get_settings().copy()
163 config = baseapp.config.get_settings().copy()
164 config['vcs.scm_app_implementation'] = (
164 config['vcs.scm_app_implementation'] = (
165 'rhodecode.tests.lib.middleware.mock_scm_app')
165 'rhodecode.tests.lib.middleware.mock_scm_app')
166 controller = StubVCSController(config, request_stub.registry)
166 controller = StubVCSController(config, request_stub.registry)
167 assert controller.scm_app is mock_scm_app
167 assert controller.scm_app is mock_scm_app
168
168
169
169
170 @pytest.mark.parametrize('query_string, expected', [
170 @pytest.mark.parametrize('query_string, expected', [
171 ('cmd=stub_command', True),
171 ('cmd=stub_command', True),
172 ('cmd=listkeys', False),
172 ('cmd=listkeys', False),
173 ])
173 ])
174 def test_should_check_locking(query_string, expected):
174 def test_should_check_locking(query_string, expected):
175 result = simplevcs._should_check_locking(query_string)
175 result = simplevcs._should_check_locking(query_string)
176 assert result == expected
176 assert result == expected
177
177
178
178
179 class TestShadowRepoRegularExpression(object):
179 class TestShadowRepoRegularExpression(object):
180 pr_segment = 'pull-request'
180 pr_segment = 'pull-request'
181 shadow_segment = 'repository'
181 shadow_segment = 'repository'
182
182
183 @pytest.mark.parametrize('url, expected', [
183 @pytest.mark.parametrize('url, expected', [
184 # repo with/without groups
184 # repo with/without groups
185 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
185 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
186 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
186 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
187 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
187 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
188 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
188 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
189
189
190 # pull request ID
190 # pull request ID
191 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
191 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
192 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
192 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
193 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
193 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
194 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
194 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
195
195
196 # unicode
196 # unicode
197 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
197 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
198 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
198 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
199
199
200 # trailing/leading slash
200 # trailing/leading slash
201 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
201 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
202 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
202 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
203 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
203 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
204
204
205 # misc
205 # misc
206 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
206 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
207 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
207 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
208 ])
208 ])
209 def test_shadow_repo_regular_expression(self, url, expected):
209 def test_shadow_repo_regular_expression(self, url, expected):
210 from rhodecode.lib.middleware.simplevcs import SimpleVCS
210 from rhodecode.lib.middleware.simplevcs import SimpleVCS
211 url = url.format(
211 url = url.format(
212 pr_segment=self.pr_segment,
212 pr_segment=self.pr_segment,
213 shadow_segment=self.shadow_segment)
213 shadow_segment=self.shadow_segment)
214 match_obj = SimpleVCS.shadow_repo_re.match(url)
214 match_obj = SimpleVCS.shadow_repo_re.match(url)
215 assert (match_obj is not None) == expected
215 assert (match_obj is not None) == expected
216
216
217
217
218 @pytest.mark.backends('git', 'hg')
218 @pytest.mark.backends('git', 'hg')
219 class TestShadowRepoExposure(object):
219 class TestShadowRepoExposure(object):
220
220
221 def test_pull_on_shadow_repo_propagates_to_wsgi_app(
221 def test_pull_on_shadow_repo_propagates_to_wsgi_app(
222 self, baseapp, request_stub):
222 self, baseapp, request_stub):
223 """
223 """
224 Check that a pull action to a shadow repo is propagated to the
224 Check that a pull action to a shadow repo is propagated to the
225 underlying wsgi app.
225 underlying wsgi app.
226 """
226 """
227 controller = StubVCSController(
227 controller = StubVCSController(
228 baseapp.config.get_settings(), request_stub.registry)
228 baseapp.config.get_settings(), request_stub.registry)
229 controller._check_ssl = mock.Mock()
229 controller._check_ssl = mock.Mock()
230 controller.is_shadow_repo = True
230 controller.is_shadow_repo = True
231 controller._action = 'pull'
231 controller._action = 'pull'
232 controller._is_shadow_repo_dir = True
232 controller._is_shadow_repo_dir = True
233 controller.stub_response_body = 'dummy body value'
233 controller.stub_response_body = 'dummy body value'
234 controller._get_default_cache_ttl = mock.Mock(
234 controller._get_default_cache_ttl = mock.Mock(
235 return_value=(False, 0))
235 return_value=(False, 0))
236
236
237 environ_stub = {
237 environ_stub = {
238 'HTTP_HOST': 'test.example.com',
238 'HTTP_HOST': 'test.example.com',
239 'HTTP_ACCEPT': 'application/mercurial',
239 'HTTP_ACCEPT': 'application/mercurial',
240 'REQUEST_METHOD': 'GET',
240 'REQUEST_METHOD': 'GET',
241 'wsgi.url_scheme': 'http',
241 'wsgi.url_scheme': 'http',
242 }
242 }
243
243
244 response = controller(environ_stub, mock.Mock())
244 response = controller(environ_stub, mock.Mock())
245 response_body = ''.join(response)
245 response_body = ''.join(response)
246
246
247 # Assert that we got the response from the wsgi app.
247 # Assert that we got the response from the wsgi app.
248 assert response_body == controller.stub_response_body
248 assert response_body == controller.stub_response_body
249
249
250 def test_pull_on_shadow_repo_that_is_missing(self, baseapp, request_stub):
250 def test_pull_on_shadow_repo_that_is_missing(self, baseapp, request_stub):
251 """
251 """
252 Check that a pull action to a shadow repo is propagated to the
252 Check that a pull action to a shadow repo is propagated to the
253 underlying wsgi app.
253 underlying wsgi app.
254 """
254 """
255 controller = StubVCSController(
255 controller = StubVCSController(
256 baseapp.config.get_settings(), request_stub.registry)
256 baseapp.config.get_settings(), request_stub.registry)
257 controller._check_ssl = mock.Mock()
257 controller._check_ssl = mock.Mock()
258 controller.is_shadow_repo = True
258 controller.is_shadow_repo = True
259 controller._action = 'pull'
259 controller._action = 'pull'
260 controller._is_shadow_repo_dir = False
260 controller._is_shadow_repo_dir = False
261 controller.stub_response_body = 'dummy body value'
261 controller.stub_response_body = 'dummy body value'
262 environ_stub = {
262 environ_stub = {
263 'HTTP_HOST': 'test.example.com',
263 'HTTP_HOST': 'test.example.com',
264 'HTTP_ACCEPT': 'application/mercurial',
264 'HTTP_ACCEPT': 'application/mercurial',
265 'REQUEST_METHOD': 'GET',
265 'REQUEST_METHOD': 'GET',
266 'wsgi.url_scheme': 'http',
266 'wsgi.url_scheme': 'http',
267 }
267 }
268
268
269 response = controller(environ_stub, mock.Mock())
269 response = controller(environ_stub, mock.Mock())
270 response_body = ''.join(response)
270 response_body = ''.join(response)
271
271
272 # Assert that we got the response from the wsgi app.
272 # Assert that we got the response from the wsgi app.
273 assert '404 Not Found' in response_body
273 assert '404 Not Found' in response_body
274
274
275 def test_push_on_shadow_repo_raises(self, baseapp, request_stub):
275 def test_push_on_shadow_repo_raises(self, baseapp, request_stub):
276 """
276 """
277 Check that a push action to a shadow repo is aborted.
277 Check that a push action to a shadow repo is aborted.
278 """
278 """
279 controller = StubVCSController(
279 controller = StubVCSController(
280 baseapp.config.get_settings(), request_stub.registry)
280 baseapp.config.get_settings(), request_stub.registry)
281 controller._check_ssl = mock.Mock()
281 controller._check_ssl = mock.Mock()
282 controller.is_shadow_repo = True
282 controller.is_shadow_repo = True
283 controller._action = 'push'
283 controller._action = 'push'
284 controller.stub_response_body = 'dummy body value'
284 controller.stub_response_body = 'dummy body value'
285 environ_stub = {
285 environ_stub = {
286 'HTTP_HOST': 'test.example.com',
286 'HTTP_HOST': 'test.example.com',
287 'HTTP_ACCEPT': 'application/mercurial',
287 'HTTP_ACCEPT': 'application/mercurial',
288 'REQUEST_METHOD': 'GET',
288 'REQUEST_METHOD': 'GET',
289 'wsgi.url_scheme': 'http',
289 'wsgi.url_scheme': 'http',
290 }
290 }
291
291
292 response = controller(environ_stub, mock.Mock())
292 response = controller(environ_stub, mock.Mock())
293 response_body = ''.join(response)
293 response_body = ''.join(response)
294
294
295 assert response_body != controller.stub_response_body
295 assert response_body != controller.stub_response_body
296 # Assert that a 406 error is returned.
296 # Assert that a 406 error is returned.
297 assert '406 Not Acceptable' in response_body
297 assert '406 Not Acceptable' in response_body
298
298
299 def test_set_repo_names_no_shadow(self, baseapp, request_stub):
299 def test_set_repo_names_no_shadow(self, baseapp, request_stub):
300 """
300 """
301 Check that the set_repo_names method sets all names to the one returned
301 Check that the set_repo_names method sets all names to the one returned
302 by the _get_repository_name method on a request to a non shadow repo.
302 by the _get_repository_name method on a request to a non shadow repo.
303 """
303 """
304 environ_stub = {}
304 environ_stub = {}
305 controller = StubVCSController(
305 controller = StubVCSController(
306 baseapp.config.get_settings(), request_stub.registry)
306 baseapp.config.get_settings(), request_stub.registry)
307 controller._name = 'RepoGroup/MyRepo'
307 controller._name = 'RepoGroup/MyRepo'
308 controller.set_repo_names(environ_stub)
308 controller.set_repo_names(environ_stub)
309 assert not controller.is_shadow_repo
309 assert not controller.is_shadow_repo
310 assert (controller.url_repo_name ==
310 assert (controller.url_repo_name ==
311 controller.acl_repo_name ==
311 controller.acl_repo_name ==
312 controller.vcs_repo_name ==
312 controller.vcs_repo_name ==
313 controller._get_repository_name(environ_stub))
313 controller._get_repository_name(environ_stub))
314
314
315 def test_set_repo_names_with_shadow(
315 def test_set_repo_names_with_shadow(
316 self, baseapp, pr_util, config_stub, request_stub):
316 self, baseapp, pr_util, config_stub, request_stub):
317 """
317 """
318 Check that the set_repo_names method sets correct names on a request
318 Check that the set_repo_names method sets correct names on a request
319 to a shadow repo.
319 to a shadow repo.
320 """
320 """
321 from rhodecode.model.pull_request import PullRequestModel
321 from rhodecode.model.pull_request import PullRequestModel
322
322
323 pull_request = pr_util.create_pull_request()
323 pull_request = pr_util.create_pull_request()
324 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
324 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
325 target=pull_request.target_repo.repo_name,
325 target=pull_request.target_repo.repo_name,
326 pr_id=pull_request.pull_request_id,
326 pr_id=pull_request.pull_request_id,
327 pr_segment=TestShadowRepoRegularExpression.pr_segment,
327 pr_segment=TestShadowRepoRegularExpression.pr_segment,
328 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
328 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
329 controller = StubVCSController(
329 controller = StubVCSController(
330 baseapp.config.get_settings(), request_stub.registry)
330 baseapp.config.get_settings(), request_stub.registry)
331 controller._name = shadow_url
331 controller._name = shadow_url
332 controller.set_repo_names({})
332 controller.set_repo_names({})
333
333
334 # Get file system path to shadow repo for assertions.
334 # Get file system path to shadow repo for assertions.
335 workspace_id = PullRequestModel()._workspace_id(pull_request)
335 workspace_id = PullRequestModel()._workspace_id(pull_request)
336 target_vcs = pull_request.target_repo.scm_instance()
336 target_vcs = pull_request.target_repo.scm_instance()
337 vcs_repo_name = target_vcs._get_shadow_repository_path(
337 vcs_repo_name = target_vcs._get_shadow_repository_path(
338 pull_request.target_repo.repo_id, workspace_id)
338 pull_request.target_repo.repo_id, workspace_id)
339
339
340 assert controller.vcs_repo_name == vcs_repo_name
340 assert controller.vcs_repo_name == vcs_repo_name
341 assert controller.url_repo_name == shadow_url
341 assert controller.url_repo_name == shadow_url
342 assert controller.acl_repo_name == pull_request.target_repo.repo_name
342 assert controller.acl_repo_name == pull_request.target_repo.repo_name
343 assert controller.is_shadow_repo
343 assert controller.is_shadow_repo
344
344
345 def test_set_repo_names_with_shadow_but_missing_pr(
345 def test_set_repo_names_with_shadow_but_missing_pr(
346 self, baseapp, pr_util, config_stub, request_stub):
346 self, baseapp, pr_util, config_stub, request_stub):
347 """
347 """
348 Checks that the set_repo_names method enforces matching target repos
348 Checks that the set_repo_names method enforces matching target repos
349 and pull request IDs.
349 and pull request IDs.
350 """
350 """
351 pull_request = pr_util.create_pull_request()
351 pull_request = pr_util.create_pull_request()
352 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
352 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
353 target=pull_request.target_repo.repo_name,
353 target=pull_request.target_repo.repo_name,
354 pr_id=999999999,
354 pr_id=999999999,
355 pr_segment=TestShadowRepoRegularExpression.pr_segment,
355 pr_segment=TestShadowRepoRegularExpression.pr_segment,
356 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
356 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
357 controller = StubVCSController(
357 controller = StubVCSController(
358 baseapp.config.get_settings(), request_stub.registry)
358 baseapp.config.get_settings(), request_stub.registry)
359 controller._name = shadow_url
359 controller._name = shadow_url
360 controller.set_repo_names({})
360 controller.set_repo_names({})
361
361
362 assert not controller.is_shadow_repo
362 assert not controller.is_shadow_repo
363 assert (controller.url_repo_name ==
363 assert (controller.url_repo_name ==
364 controller.acl_repo_name ==
364 controller.acl_repo_name ==
365 controller.vcs_repo_name)
365 controller.vcs_repo_name)
366
366
367
367
368 @pytest.mark.usefixtures('baseapp')
368 @pytest.mark.usefixtures('baseapp')
369 class TestGenerateVcsResponse(object):
369 class TestGenerateVcsResponse(object):
370
370
371 def test_ensures_that_start_response_is_called_early_enough(self):
371 def test_ensures_that_start_response_is_called_early_enough(self):
372 self.call_controller_with_response_body(iter(['a', 'b']))
372 self.call_controller_with_response_body(iter(['a', 'b']))
373 assert self.start_response.called
373 assert self.start_response.called
374
374
375 def test_invalidates_cache_after_body_is_consumed(self):
375 def test_invalidates_cache_after_body_is_consumed(self):
376 result = self.call_controller_with_response_body(iter(['a', 'b']))
376 result = self.call_controller_with_response_body(iter(['a', 'b']))
377 assert not self.was_cache_invalidated()
377 assert not self.was_cache_invalidated()
378 # Consume the result
378 # Consume the result
379 list(result)
379 list(result)
380 assert self.was_cache_invalidated()
380 assert self.was_cache_invalidated()
381
381
382 def test_raises_unknown_exceptions(self):
382 def test_raises_unknown_exceptions(self):
383 result = self.call_controller_with_response_body(
383 result = self.call_controller_with_response_body(
384 self.raise_result_iter(vcs_kind='unknown'))
384 self.raise_result_iter(vcs_kind='unknown'))
385 with pytest.raises(Exception):
385 with pytest.raises(Exception):
386 list(result)
386 list(result)
387
387
388 def test_prepare_callback_daemon_is_called(self):
388 def test_prepare_callback_daemon_is_called(self):
389 def side_effect(extras, environ, action, txn_id=None):
389 def side_effect(extras, environ, action, txn_id=None):
390 return DummyHooksCallbackDaemon(), extras
390 return DummyHooksCallbackDaemon(), extras
391
391
392 prepare_patcher = mock.patch.object(
392 prepare_patcher = mock.patch.object(
393 StubVCSController, '_prepare_callback_daemon')
393 StubVCSController, '_prepare_callback_daemon')
394 with prepare_patcher as prepare_mock:
394 with prepare_patcher as prepare_mock:
395 prepare_mock.side_effect = side_effect
395 prepare_mock.side_effect = side_effect
396 self.call_controller_with_response_body(iter(['a', 'b']))
396 self.call_controller_with_response_body(iter(['a', 'b']))
397 assert prepare_mock.called
397 assert prepare_mock.called
398 assert prepare_mock.call_count == 1
398 assert prepare_mock.call_count == 1
399
399
400 def call_controller_with_response_body(self, response_body):
400 def call_controller_with_response_body(self, response_body):
401 settings = {
401 settings = {
402 'base_path': 'fake_base_path',
402 'base_path': 'fake_base_path',
403 'vcs.hooks.protocol': 'http',
403 'vcs.hooks.protocol': 'http',
404 'vcs.hooks.direct_calls': False,
404 'vcs.hooks.direct_calls': False,
405 }
405 }
406 registry = AttributeDict()
406 registry = AttributeDict()
407 controller = StubVCSController(settings, registry)
407 controller = StubVCSController(settings, registry)
408 controller._invalidate_cache = mock.Mock()
408 controller._invalidate_cache = mock.Mock()
409 controller.stub_response_body = response_body
409 controller.stub_response_body = response_body
410 self.start_response = mock.Mock()
410 self.start_response = mock.Mock()
411 result = controller._generate_vcs_response(
411 result = controller._generate_vcs_response(
412 environ={}, start_response=self.start_response,
412 environ={}, start_response=self.start_response,
413 repo_path='fake_repo_path',
413 repo_path='fake_repo_path',
414 extras={}, action='push')
414 extras={}, action='push')
415 self.controller = controller
415 self.controller = controller
416 return result
416 return result
417
417
418 def raise_result_iter(self, vcs_kind='repo_locked'):
418 def raise_result_iter(self, vcs_kind='repo_locked'):
419 """
419 """
420 Simulates an exception due to a vcs raised exception if kind vcs_kind
420 Simulates an exception due to a vcs raised exception if kind vcs_kind
421 """
421 """
422 raise self.vcs_exception(vcs_kind=vcs_kind)
422 raise self.vcs_exception(vcs_kind=vcs_kind)
423 yield "never_reached"
423 yield "never_reached"
424
424
425 def vcs_exception(self, vcs_kind='repo_locked'):
425 def vcs_exception(self, vcs_kind='repo_locked'):
426 locked_exception = Exception('TEST_MESSAGE')
426 locked_exception = Exception('TEST_MESSAGE')
427 locked_exception._vcs_kind = vcs_kind
427 locked_exception._vcs_kind = vcs_kind
428 return locked_exception
428 return locked_exception
429
429
430 def was_cache_invalidated(self):
430 def was_cache_invalidated(self):
431 return self.controller._invalidate_cache.called
431 return self.controller._invalidate_cache.called
432
432
433
433
434 class TestInitializeGenerator(object):
434 class TestInitializeGenerator(object):
435
435
436 def test_drains_first_element(self):
436 def test_drains_first_element(self):
437 gen = self.factory(['__init__', 1, 2])
437 gen = self.factory(['__init__', 1, 2])
438 result = list(gen)
438 result = list(gen)
439 assert result == [1, 2]
439 assert result == [1, 2]
440
440
441 @pytest.mark.parametrize('values', [
441 @pytest.mark.parametrize('values', [
442 [],
442 [],
443 [1, 2],
443 [1, 2],
444 ])
444 ])
445 def test_raises_value_error(self, values):
445 def test_raises_value_error(self, values):
446 with pytest.raises(ValueError):
446 with pytest.raises(ValueError):
447 self.factory(values)
447 self.factory(values)
448
448
449 @simplevcs.initialize_generator
449 @simplevcs.initialize_generator
450 def factory(self, iterable):
450 def factory(self, iterable):
451 for elem in iterable:
451 for elem in iterable:
452 yield elem
452 yield elem
453
453
454
454
455 class TestPrepareHooksDaemon(object):
455 class TestPrepareHooksDaemon(object):
456 def test_calls_imported_prepare_callback_daemon(self, app_settings, request_stub):
456 def test_calls_imported_prepare_callback_daemon(self, app_settings, request_stub):
457 expected_extras = {'extra1': 'value1'}
457 expected_extras = {'extra1': 'value1'}
458 daemon = DummyHooksCallbackDaemon()
458 daemon = DummyHooksCallbackDaemon()
459
459
460 controller = StubVCSController(app_settings, request_stub.registry)
460 controller = StubVCSController(app_settings, request_stub.registry)
461 prepare_patcher = mock.patch.object(
461 prepare_patcher = mock.patch.object(
462 simplevcs, 'prepare_callback_daemon',
462 simplevcs, 'prepare_callback_daemon',
463 return_value=(daemon, expected_extras))
463 return_value=(daemon, expected_extras))
464 with prepare_patcher as prepare_mock:
464 with prepare_patcher as prepare_mock:
465 callback_daemon, extras = controller._prepare_callback_daemon(
465 callback_daemon, extras = controller._prepare_callback_daemon(
466 expected_extras.copy(), {}, 'push')
466 expected_extras.copy(), {}, 'push')
467 prepare_mock.assert_called_once_with(
467 prepare_mock.assert_called_once_with(
468 expected_extras,
468 expected_extras,
469 protocol=app_settings['vcs.hooks.protocol'],
469 protocol=app_settings['vcs.hooks.protocol'],
470 host=app_settings['vcs.hooks.host'],
470 host=app_settings['vcs.hooks.host'],
471 txn_id=None,
471 txn_id=None,
472 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
472 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
473
473
474 assert callback_daemon == daemon
474 assert callback_daemon == daemon
475 assert extras == extras
475 assert extras == extras
General Comments 0
You need to be logged in to leave comments. Login now