##// END OF EJS Templates
caches: fix import of compute_key_from_params function.
marcink -
r2970:20adf10a default
parent child Browse files
Show More
@@ -1,313 +1,312 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import time
22 import time
23 import logging
23 import logging
24 import tempfile
24 import tempfile
25 import traceback
25 import traceback
26 import threading
26 import threading
27
27
28 from BaseHTTPServer import BaseHTTPRequestHandler
28 from BaseHTTPServer import BaseHTTPRequestHandler
29 from SocketServer import TCPServer
29 from SocketServer import TCPServer
30
30
31 import rhodecode
31 import rhodecode
32 from rhodecode.model import meta
32 from rhodecode.model import meta
33 from rhodecode.lib.base import bootstrap_request, bootstrap_config
33 from rhodecode.lib.base import bootstrap_request, bootstrap_config
34 from rhodecode.lib import hooks_base
34 from rhodecode.lib import hooks_base
35 from rhodecode.lib.utils2 import AttributeDict
35 from rhodecode.lib.utils2 import AttributeDict
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37
37 from rhodecode.lib import rc_cache
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class HooksHttpHandler(BaseHTTPRequestHandler):
42 class HooksHttpHandler(BaseHTTPRequestHandler):
43
43
44 def do_POST(self):
44 def do_POST(self):
45 method, extras = self._read_request()
45 method, extras = self._read_request()
46 txn_id = getattr(self.server, 'txn_id', None)
46 txn_id = getattr(self.server, 'txn_id', None)
47 if txn_id:
47 if txn_id:
48 from rhodecode.lib.caches import compute_key_from_params
49 log.debug('Computing TXN_ID based on `%s`:`%s`',
48 log.debug('Computing TXN_ID based on `%s`:`%s`',
50 extras['repository'], extras['txn_id'])
49 extras['repository'], extras['txn_id'])
51 computed_txn_id = compute_key_from_params(
50 computed_txn_id = rc_cache.utils.compute_key_from_params(
52 extras['repository'], extras['txn_id'])
51 extras['repository'], extras['txn_id'])
53 if txn_id != computed_txn_id:
52 if txn_id != computed_txn_id:
54 raise Exception(
53 raise Exception(
55 'TXN ID fail: expected {} got {} instead'.format(
54 'TXN ID fail: expected {} got {} instead'.format(
56 txn_id, computed_txn_id))
55 txn_id, computed_txn_id))
57
56
58 try:
57 try:
59 result = self._call_hook(method, extras)
58 result = self._call_hook(method, extras)
60 except Exception as e:
59 except Exception as e:
61 exc_tb = traceback.format_exc()
60 exc_tb = traceback.format_exc()
62 result = {
61 result = {
63 'exception': e.__class__.__name__,
62 'exception': e.__class__.__name__,
64 'exception_traceback': exc_tb,
63 'exception_traceback': exc_tb,
65 'exception_args': e.args
64 'exception_args': e.args
66 }
65 }
67 self._write_response(result)
66 self._write_response(result)
68
67
69 def _read_request(self):
68 def _read_request(self):
70 length = int(self.headers['Content-Length'])
69 length = int(self.headers['Content-Length'])
71 body = self.rfile.read(length).decode('utf-8')
70 body = self.rfile.read(length).decode('utf-8')
72 data = json.loads(body)
71 data = json.loads(body)
73 return data['method'], data['extras']
72 return data['method'], data['extras']
74
73
75 def _write_response(self, result):
74 def _write_response(self, result):
76 self.send_response(200)
75 self.send_response(200)
77 self.send_header("Content-type", "text/json")
76 self.send_header("Content-type", "text/json")
78 self.end_headers()
77 self.end_headers()
79 self.wfile.write(json.dumps(result))
78 self.wfile.write(json.dumps(result))
80
79
81 def _call_hook(self, method, extras):
80 def _call_hook(self, method, extras):
82 hooks = Hooks()
81 hooks = Hooks()
83 try:
82 try:
84 result = getattr(hooks, method)(extras)
83 result = getattr(hooks, method)(extras)
85 finally:
84 finally:
86 meta.Session.remove()
85 meta.Session.remove()
87 return result
86 return result
88
87
89 def log_message(self, format, *args):
88 def log_message(self, format, *args):
90 """
89 """
91 This is an overridden method of BaseHTTPRequestHandler which logs using
90 This is an overridden method of BaseHTTPRequestHandler which logs using
92 logging library instead of writing directly to stderr.
91 logging library instead of writing directly to stderr.
93 """
92 """
94
93
95 message = format % args
94 message = format % args
96
95
97 log.debug(
96 log.debug(
98 "%s - - [%s] %s", self.client_address[0],
97 "%s - - [%s] %s", self.client_address[0],
99 self.log_date_time_string(), message)
98 self.log_date_time_string(), message)
100
99
101
100
102 class DummyHooksCallbackDaemon(object):
101 class DummyHooksCallbackDaemon(object):
103 hooks_uri = ''
102 hooks_uri = ''
104
103
105 def __init__(self):
104 def __init__(self):
106 self.hooks_module = Hooks.__module__
105 self.hooks_module = Hooks.__module__
107
106
108 def __enter__(self):
107 def __enter__(self):
109 log.debug('Running dummy hooks callback daemon')
108 log.debug('Running dummy hooks callback daemon')
110 return self
109 return self
111
110
112 def __exit__(self, exc_type, exc_val, exc_tb):
111 def __exit__(self, exc_type, exc_val, exc_tb):
113 log.debug('Exiting dummy hooks callback daemon')
112 log.debug('Exiting dummy hooks callback daemon')
114
113
115
114
116 class ThreadedHookCallbackDaemon(object):
115 class ThreadedHookCallbackDaemon(object):
117
116
118 _callback_thread = None
117 _callback_thread = None
119 _daemon = None
118 _daemon = None
120 _done = False
119 _done = False
121
120
122 def __init__(self, txn_id=None, host=None, port=None):
121 def __init__(self, txn_id=None, host=None, port=None):
123 self._prepare(txn_id=txn_id, host=None, port=port)
122 self._prepare(txn_id=txn_id, host=None, port=port)
124
123
125 def __enter__(self):
124 def __enter__(self):
126 self._run()
125 self._run()
127 return self
126 return self
128
127
129 def __exit__(self, exc_type, exc_val, exc_tb):
128 def __exit__(self, exc_type, exc_val, exc_tb):
130 log.debug('Callback daemon exiting now...')
129 log.debug('Callback daemon exiting now...')
131 self._stop()
130 self._stop()
132
131
133 def _prepare(self, txn_id=None, host=None, port=None):
132 def _prepare(self, txn_id=None, host=None, port=None):
134 raise NotImplementedError()
133 raise NotImplementedError()
135
134
136 def _run(self):
135 def _run(self):
137 raise NotImplementedError()
136 raise NotImplementedError()
138
137
139 def _stop(self):
138 def _stop(self):
140 raise NotImplementedError()
139 raise NotImplementedError()
141
140
142
141
143 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
142 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
144 """
143 """
145 Context manager which will run a callback daemon in a background thread.
144 Context manager which will run a callback daemon in a background thread.
146 """
145 """
147
146
148 hooks_uri = None
147 hooks_uri = None
149
148
150 # From Python docs: Polling reduces our responsiveness to a shutdown
149 # From Python docs: Polling reduces our responsiveness to a shutdown
151 # request and wastes cpu at all other times.
150 # request and wastes cpu at all other times.
152 POLL_INTERVAL = 0.01
151 POLL_INTERVAL = 0.01
153
152
154 def _prepare(self, txn_id=None, host=None, port=None):
153 def _prepare(self, txn_id=None, host=None, port=None):
155 host = host or '127.0.0.1'
154 host = host or '127.0.0.1'
156 self._done = False
155 self._done = False
157 self._daemon = TCPServer((host, port or 0), HooksHttpHandler)
156 self._daemon = TCPServer((host, port or 0), HooksHttpHandler)
158 _, port = self._daemon.server_address
157 _, port = self._daemon.server_address
159 self.hooks_uri = '{}:{}'.format(host, port)
158 self.hooks_uri = '{}:{}'.format(host, port)
160 self.txn_id = txn_id
159 self.txn_id = txn_id
161 # inject transaction_id for later verification
160 # inject transaction_id for later verification
162 self._daemon.txn_id = self.txn_id
161 self._daemon.txn_id = self.txn_id
163
162
164 log.debug(
163 log.debug(
165 "Preparing HTTP callback daemon at `%s` and registering hook object",
164 "Preparing HTTP callback daemon at `%s` and registering hook object",
166 self.hooks_uri)
165 self.hooks_uri)
167
166
168 def _run(self):
167 def _run(self):
169 log.debug("Running event loop of callback daemon in background thread")
168 log.debug("Running event loop of callback daemon in background thread")
170 callback_thread = threading.Thread(
169 callback_thread = threading.Thread(
171 target=self._daemon.serve_forever,
170 target=self._daemon.serve_forever,
172 kwargs={'poll_interval': self.POLL_INTERVAL})
171 kwargs={'poll_interval': self.POLL_INTERVAL})
173 callback_thread.daemon = True
172 callback_thread.daemon = True
174 callback_thread.start()
173 callback_thread.start()
175 self._callback_thread = callback_thread
174 self._callback_thread = callback_thread
176
175
177 def _stop(self):
176 def _stop(self):
178 log.debug("Waiting for background thread to finish.")
177 log.debug("Waiting for background thread to finish.")
179 self._daemon.shutdown()
178 self._daemon.shutdown()
180 self._callback_thread.join()
179 self._callback_thread.join()
181 self._daemon = None
180 self._daemon = None
182 self._callback_thread = None
181 self._callback_thread = None
183 if self.txn_id:
182 if self.txn_id:
184 txn_id_file = get_txn_id_data_path(self.txn_id)
183 txn_id_file = get_txn_id_data_path(self.txn_id)
185 log.debug('Cleaning up TXN ID %s', txn_id_file)
184 log.debug('Cleaning up TXN ID %s', txn_id_file)
186 if os.path.isfile(txn_id_file):
185 if os.path.isfile(txn_id_file):
187 os.remove(txn_id_file)
186 os.remove(txn_id_file)
188
187
189 log.debug("Background thread done.")
188 log.debug("Background thread done.")
190
189
191
190
192 def get_txn_id_data_path(txn_id):
191 def get_txn_id_data_path(txn_id):
193 root = tempfile.gettempdir()
192 root = tempfile.gettempdir()
194 return os.path.join(root, 'rc_txn_id_{}'.format(txn_id))
193 return os.path.join(root, 'rc_txn_id_{}'.format(txn_id))
195
194
196
195
197 def store_txn_id_data(txn_id, data_dict):
196 def store_txn_id_data(txn_id, data_dict):
198 if not txn_id:
197 if not txn_id:
199 log.warning('Cannot store txn_id because it is empty')
198 log.warning('Cannot store txn_id because it is empty')
200 return
199 return
201
200
202 path = get_txn_id_data_path(txn_id)
201 path = get_txn_id_data_path(txn_id)
203 try:
202 try:
204 with open(path, 'wb') as f:
203 with open(path, 'wb') as f:
205 f.write(json.dumps(data_dict))
204 f.write(json.dumps(data_dict))
206 except Exception:
205 except Exception:
207 log.exception('Failed to write txn_id metadata')
206 log.exception('Failed to write txn_id metadata')
208
207
209
208
210 def get_txn_id_from_store(txn_id):
209 def get_txn_id_from_store(txn_id):
211 """
210 """
212 Reads txn_id from store and if present returns the data for callback manager
211 Reads txn_id from store and if present returns the data for callback manager
213 """
212 """
214 path = get_txn_id_data_path(txn_id)
213 path = get_txn_id_data_path(txn_id)
215 try:
214 try:
216 with open(path, 'rb') as f:
215 with open(path, 'rb') as f:
217 return json.loads(f.read())
216 return json.loads(f.read())
218 except Exception:
217 except Exception:
219 return {}
218 return {}
220
219
221
220
222 def prepare_callback_daemon(extras, protocol, host, use_direct_calls, txn_id=None):
221 def prepare_callback_daemon(extras, protocol, host, use_direct_calls, txn_id=None):
223 txn_details = get_txn_id_from_store(txn_id)
222 txn_details = get_txn_id_from_store(txn_id)
224 port = txn_details.get('port', 0)
223 port = txn_details.get('port', 0)
225 if use_direct_calls:
224 if use_direct_calls:
226 callback_daemon = DummyHooksCallbackDaemon()
225 callback_daemon = DummyHooksCallbackDaemon()
227 extras['hooks_module'] = callback_daemon.hooks_module
226 extras['hooks_module'] = callback_daemon.hooks_module
228 else:
227 else:
229 if protocol == 'http':
228 if protocol == 'http':
230 callback_daemon = HttpHooksCallbackDaemon(
229 callback_daemon = HttpHooksCallbackDaemon(
231 txn_id=txn_id, host=host, port=port)
230 txn_id=txn_id, host=host, port=port)
232 else:
231 else:
233 log.error('Unsupported callback daemon protocol "%s"', protocol)
232 log.error('Unsupported callback daemon protocol "%s"', protocol)
234 raise Exception('Unsupported callback daemon protocol.')
233 raise Exception('Unsupported callback daemon protocol.')
235
234
236 extras['hooks_uri'] = callback_daemon.hooks_uri
235 extras['hooks_uri'] = callback_daemon.hooks_uri
237 extras['hooks_protocol'] = protocol
236 extras['hooks_protocol'] = protocol
238 extras['time'] = time.time()
237 extras['time'] = time.time()
239
238
240 # register txn_id
239 # register txn_id
241 extras['txn_id'] = txn_id
240 extras['txn_id'] = txn_id
242
241
243 log.debug('Prepared a callback daemon: %s at url `%s`',
242 log.debug('Prepared a callback daemon: %s at url `%s`',
244 callback_daemon.__class__.__name__, callback_daemon.hooks_uri)
243 callback_daemon.__class__.__name__, callback_daemon.hooks_uri)
245 return callback_daemon, extras
244 return callback_daemon, extras
246
245
247
246
248 class Hooks(object):
247 class Hooks(object):
249 """
248 """
250 Exposes the hooks for remote call backs
249 Exposes the hooks for remote call backs
251 """
250 """
252
251
253 def repo_size(self, extras):
252 def repo_size(self, extras):
254 log.debug("Called repo_size of %s object", self)
253 log.debug("Called repo_size of %s object", self)
255 return self._call_hook(hooks_base.repo_size, extras)
254 return self._call_hook(hooks_base.repo_size, extras)
256
255
257 def pre_pull(self, extras):
256 def pre_pull(self, extras):
258 log.debug("Called pre_pull of %s object", self)
257 log.debug("Called pre_pull of %s object", self)
259 return self._call_hook(hooks_base.pre_pull, extras)
258 return self._call_hook(hooks_base.pre_pull, extras)
260
259
261 def post_pull(self, extras):
260 def post_pull(self, extras):
262 log.debug("Called post_pull of %s object", self)
261 log.debug("Called post_pull of %s object", self)
263 return self._call_hook(hooks_base.post_pull, extras)
262 return self._call_hook(hooks_base.post_pull, extras)
264
263
265 def pre_push(self, extras):
264 def pre_push(self, extras):
266 log.debug("Called pre_push of %s object", self)
265 log.debug("Called pre_push of %s object", self)
267 return self._call_hook(hooks_base.pre_push, extras)
266 return self._call_hook(hooks_base.pre_push, extras)
268
267
269 def post_push(self, extras):
268 def post_push(self, extras):
270 log.debug("Called post_push of %s object", self)
269 log.debug("Called post_push of %s object", self)
271 return self._call_hook(hooks_base.post_push, extras)
270 return self._call_hook(hooks_base.post_push, extras)
272
271
273 def _call_hook(self, hook, extras):
272 def _call_hook(self, hook, extras):
274 extras = AttributeDict(extras)
273 extras = AttributeDict(extras)
275 server_url = extras['server_url']
274 server_url = extras['server_url']
276 request = bootstrap_request(application_url=server_url)
275 request = bootstrap_request(application_url=server_url)
277
276
278 bootstrap_config(request) # inject routes and other interfaces
277 bootstrap_config(request) # inject routes and other interfaces
279
278
280 # inject the user for usage in hooks
279 # inject the user for usage in hooks
281 request.user = AttributeDict({'username': extras.username,
280 request.user = AttributeDict({'username': extras.username,
282 'ip_addr': extras.ip,
281 'ip_addr': extras.ip,
283 'user_id': extras.user_id})
282 'user_id': extras.user_id})
284
283
285 extras.request = request
284 extras.request = request
286
285
287 try:
286 try:
288 result = hook(extras)
287 result = hook(extras)
289 except Exception as error:
288 except Exception as error:
290 exc_tb = traceback.format_exc()
289 exc_tb = traceback.format_exc()
291 log.exception('Exception when handling hook %s', hook)
290 log.exception('Exception when handling hook %s', hook)
292 error_args = error.args
291 error_args = error.args
293 return {
292 return {
294 'status': 128,
293 'status': 128,
295 'output': '',
294 'output': '',
296 'exception': type(error).__name__,
295 'exception': type(error).__name__,
297 'exception_traceback': exc_tb,
296 'exception_traceback': exc_tb,
298 'exception_args': error_args,
297 'exception_args': error_args,
299 }
298 }
300 finally:
299 finally:
301 meta.Session.remove()
300 meta.Session.remove()
302
301
303 log.debug('Got hook call response %s', result)
302 log.debug('Got hook call response %s', result)
304 return {
303 return {
305 'status': result.status,
304 'status': result.status,
306 'output': result.output,
305 'output': result.output,
307 }
306 }
308
307
309 def __enter__(self):
308 def __enter__(self):
310 return self
309 return self
311
310
312 def __exit__(self, exc_type, exc_val, exc_tb):
311 def __exit__(self, exc_type, exc_val, exc_tb):
313 pass
312 pass
@@ -1,204 +1,204 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 from urlparse import urljoin
24 from urlparse import urljoin
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
51
52 data = environ['wsgi.input']
52 data = environ['wsgi.input']
53 # johbo: Avoid that we end up with sending the request in chunked
53 # johbo: Avoid that we end up with sending the request in chunked
54 # transfer encoding (mainly on Gunicorn). If we know the content
54 # transfer encoding (mainly on Gunicorn). If we know the content
55 # length, then we should transfer the payload in one request.
55 # length, then we should transfer the payload in one request.
56 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
56 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
57 data = data.read()
57 data = data.read()
58 if data.startswith('(create-txn-with-props'):
58 if data.startswith('(create-txn-with-props'):
59 # store on-the-fly our rc_extra using svn revision properties
59 # store on-the-fly our rc_extra using svn revision properties
60 # those can be read later on in hooks executed so we have a way
60 # those can be read later on in hooks executed so we have a way
61 # to pass in the data into svn hooks
61 # to pass in the data into svn hooks
62 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
62 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
63 rc_data_len = len(rc_data)
63 rc_data_len = len(rc_data)
64 # header defines data lenght, and serialized data
64 # header defines data lenght, and serialized data
65 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
65 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
66 data = data[:-2] + skel + '))'
66 data = data[:-2] + skel + '))'
67
67
68 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
68 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
69 self._get_url(environ['PATH_INFO']))
69 self._get_url(environ['PATH_INFO']))
70
70
71 response = requests.request(
71 response = requests.request(
72 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
72 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
73 data=data, headers=request_headers)
73 data=data, headers=request_headers)
74
74
75 if response.status_code not in [200, 401]:
75 if response.status_code not in [200, 401]:
76 if response.status_code >= 500:
76 if response.status_code >= 500:
77 log.error('Got SVN response:%s with text:`%s`',
77 log.error('Got SVN response:%s with text:`%s`',
78 response, response.text)
78 response, response.text)
79 else:
79 else:
80 log.debug('Got SVN response:%s with text:`%s`',
80 log.debug('Got SVN response:%s with text:`%s`',
81 response, response.text)
81 response, response.text)
82 else:
82 else:
83 log.debug('got response code: %s', response.status_code)
83 log.debug('got response code: %s', response.status_code)
84
84
85 response_headers = self._get_response_headers(response.headers)
85 response_headers = self._get_response_headers(response.headers)
86
86
87 if response.headers.get('SVN-Txn-name'):
87 if response.headers.get('SVN-Txn-name'):
88 svn_tx_id = response.headers.get('SVN-Txn-name')
88 svn_tx_id = response.headers.get('SVN-Txn-name')
89 txn_id = rc_cache.compute_key_from_params(
89 txn_id = rc_cache.utils.compute_key_from_params(
90 self.config['repository'], svn_tx_id)
90 self.config['repository'], svn_tx_id)
91 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
91 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
92 store_txn_id_data(txn_id, {'port': port})
92 store_txn_id_data(txn_id, {'port': port})
93
93
94 start_response(
94 start_response(
95 '{} {}'.format(response.status_code, response.reason),
95 '{} {}'.format(response.status_code, response.reason),
96 response_headers)
96 response_headers)
97 return response.iter_content(chunk_size=1024)
97 return response.iter_content(chunk_size=1024)
98
98
99 def _get_url(self, path):
99 def _get_url(self, path):
100 url_path = urljoin(
100 url_path = urljoin(
101 self.config.get('subversion_http_server_url', ''), path)
101 self.config.get('subversion_http_server_url', ''), path)
102 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
102 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
103 return url_path
103 return url_path
104
104
105 def _get_request_headers(self, environ):
105 def _get_request_headers(self, environ):
106 headers = {}
106 headers = {}
107
107
108 for key in environ:
108 for key in environ:
109 if not key.startswith('HTTP_'):
109 if not key.startswith('HTTP_'):
110 continue
110 continue
111 new_key = key.split('_')
111 new_key = key.split('_')
112 new_key = [k.capitalize() for k in new_key[1:]]
112 new_key = [k.capitalize() for k in new_key[1:]]
113 new_key = '-'.join(new_key)
113 new_key = '-'.join(new_key)
114 headers[new_key] = environ[key]
114 headers[new_key] = environ[key]
115
115
116 if 'CONTENT_TYPE' in environ:
116 if 'CONTENT_TYPE' in environ:
117 headers['Content-Type'] = environ['CONTENT_TYPE']
117 headers['Content-Type'] = environ['CONTENT_TYPE']
118
118
119 if 'CONTENT_LENGTH' in environ:
119 if 'CONTENT_LENGTH' in environ:
120 headers['Content-Length'] = environ['CONTENT_LENGTH']
120 headers['Content-Length'] = environ['CONTENT_LENGTH']
121
121
122 return headers
122 return headers
123
123
124 def _get_response_headers(self, headers):
124 def _get_response_headers(self, headers):
125 headers = [
125 headers = [
126 (h, headers[h])
126 (h, headers[h])
127 for h in headers
127 for h in headers
128 if h.lower() not in self.IGNORED_HEADERS
128 if h.lower() not in self.IGNORED_HEADERS
129 ]
129 ]
130
130
131 return headers
131 return headers
132
132
133
133
134 class DisabledSimpleSvnApp(object):
134 class DisabledSimpleSvnApp(object):
135 def __init__(self, config):
135 def __init__(self, config):
136 self.config = config
136 self.config = config
137
137
138 def __call__(self, environ, start_response):
138 def __call__(self, environ, start_response):
139 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
139 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
140 log.warning(reason)
140 log.warning(reason)
141 return HTTPNotAcceptable(reason)(environ, start_response)
141 return HTTPNotAcceptable(reason)(environ, start_response)
142
142
143
143
144 class SimpleSvn(simplevcs.SimpleVCS):
144 class SimpleSvn(simplevcs.SimpleVCS):
145
145
146 SCM = 'svn'
146 SCM = 'svn'
147 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
147 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
148 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
148 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
149
149
150 def _get_repository_name(self, environ):
150 def _get_repository_name(self, environ):
151 """
151 """
152 Gets repository name out of PATH_INFO header
152 Gets repository name out of PATH_INFO header
153
153
154 :param environ: environ where PATH_INFO is stored
154 :param environ: environ where PATH_INFO is stored
155 """
155 """
156 path = environ['PATH_INFO'].split('!')
156 path = environ['PATH_INFO'].split('!')
157 repo_name = path[0].strip('/')
157 repo_name = path[0].strip('/')
158
158
159 # SVN includes the whole path in it's requests, including
159 # SVN includes the whole path in it's requests, including
160 # subdirectories inside the repo. Therefore we have to search for
160 # subdirectories inside the repo. Therefore we have to search for
161 # the repo root directory.
161 # the repo root directory.
162 if not is_valid_repo(
162 if not is_valid_repo(
163 repo_name, self.base_path, explicit_scm=self.SCM):
163 repo_name, self.base_path, explicit_scm=self.SCM):
164 current_path = ''
164 current_path = ''
165 for component in repo_name.split('/'):
165 for component in repo_name.split('/'):
166 current_path += component
166 current_path += component
167 if is_valid_repo(
167 if is_valid_repo(
168 current_path, self.base_path, explicit_scm=self.SCM):
168 current_path, self.base_path, explicit_scm=self.SCM):
169 return current_path
169 return current_path
170 current_path += '/'
170 current_path += '/'
171
171
172 return repo_name
172 return repo_name
173
173
174 def _get_action(self, environ):
174 def _get_action(self, environ):
175 return (
175 return (
176 'pull'
176 'pull'
177 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
177 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
178 else 'push')
178 else 'push')
179
179
180 def _should_use_callback_daemon(self, extras, environ, action):
180 def _should_use_callback_daemon(self, extras, environ, action):
181 # only MERGE command triggers hooks, so we don't want to start
181 # only MERGE command triggers hooks, so we don't want to start
182 # hooks server too many times. POST however starts the svn transaction
182 # hooks server too many times. POST however starts the svn transaction
183 # so we also need to run the init of callback daemon of POST
183 # so we also need to run the init of callback daemon of POST
184 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
184 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
185 return True
185 return True
186 return False
186 return False
187
187
188 def _create_wsgi_app(self, repo_path, repo_name, config):
188 def _create_wsgi_app(self, repo_path, repo_name, config):
189 if self._is_svn_enabled():
189 if self._is_svn_enabled():
190 return SimpleSvnApp(config)
190 return SimpleSvnApp(config)
191 # we don't have http proxy enabled return dummy request handler
191 # we don't have http proxy enabled return dummy request handler
192 return DisabledSimpleSvnApp(config)
192 return DisabledSimpleSvnApp(config)
193
193
194 def _is_svn_enabled(self):
194 def _is_svn_enabled(self):
195 conf = self.repo_vcs_config
195 conf = self.repo_vcs_config
196 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
196 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
197
197
198 def _create_config(self, extras, repo_name):
198 def _create_config(self, extras, repo_name):
199 conf = self.repo_vcs_config
199 conf = self.repo_vcs_config
200 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
200 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
201 server_url = server_url or self.DEFAULT_HTTP_SERVER
201 server_url = server_url or self.DEFAULT_HTTP_SERVER
202
202
203 extras['subversion_http_server_url'] = server_url
203 extras['subversion_http_server_url'] = server_url
204 return extras
204 return extras
@@ -1,661 +1,661 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 submited 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.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(
156 settings_path = self.repo_vcs_config.get(
157 *VcsSettingsModel.PATH_SETTING)
157 *VcsSettingsModel.PATH_SETTING)
158
158
159 if not settings_path:
159 if not settings_path:
160 settings_path = self.global_vcs_config.get(
160 settings_path = self.global_vcs_config.get(
161 *VcsSettingsModel.PATH_SETTING)
161 *VcsSettingsModel.PATH_SETTING)
162
162
163 if not settings_path:
163 if not settings_path:
164 # try, maybe we passed in explicitly as config option
164 # try, maybe we passed in explicitly as config option
165 settings_path = self.config.get('base_path')
165 settings_path = self.config.get('base_path')
166
166
167 if not settings_path:
167 if not settings_path:
168 raise ValueError('FATAL: base_path is empty')
168 raise ValueError('FATAL: base_path is empty')
169 return settings_path
169 return settings_path
170
170
171 def set_repo_names(self, environ):
171 def set_repo_names(self, environ):
172 """
172 """
173 This will populate the attributes acl_repo_name, url_repo_name,
173 This will populate the attributes acl_repo_name, url_repo_name,
174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 shadow) repositories all names are equal. In case of requests to a
175 shadow) repositories all names are equal. In case of requests to a
176 shadow repository the acl-name points to the target repo of the pull
176 shadow repository the acl-name points to the target repo of the pull
177 request and the vcs-name points to the shadow repo file system path.
177 request and the vcs-name points to the shadow repo file system path.
178 The url-name is always the URL used by the vcs client program.
178 The url-name is always the URL used by the vcs client program.
179
179
180 Example in case of a shadow repo:
180 Example in case of a shadow repo:
181 acl_repo_name = RepoGroup/MyRepo
181 acl_repo_name = RepoGroup/MyRepo
182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 """
184 """
185 # First we set the repo name from URL for all attributes. This is the
185 # First we set the repo name from URL for all attributes. This is the
186 # default if handling normal (non shadow) repo requests.
186 # default if handling normal (non shadow) repo requests.
187 self.url_repo_name = self._get_repository_name(environ)
187 self.url_repo_name = self._get_repository_name(environ)
188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 self.is_shadow_repo = False
189 self.is_shadow_repo = False
190
190
191 # Check if this is a request to a shadow repository.
191 # Check if this is a request to a shadow repository.
192 match = self.shadow_repo_re.match(self.url_repo_name)
192 match = self.shadow_repo_re.match(self.url_repo_name)
193 if match:
193 if match:
194 match_dict = match.groupdict()
194 match_dict = match.groupdict()
195
195
196 # Build acl repo name from regex match.
196 # Build acl repo name from regex match.
197 acl_repo_name = safe_unicode('{groups}{target}'.format(
197 acl_repo_name = safe_unicode('{groups}{target}'.format(
198 groups=match_dict['groups'] or '',
198 groups=match_dict['groups'] or '',
199 target=match_dict['target']))
199 target=match_dict['target']))
200
200
201 # Retrieve pull request instance by ID from regex match.
201 # Retrieve pull request instance by ID from regex match.
202 pull_request = PullRequest.get(match_dict['pr_id'])
202 pull_request = PullRequest.get(match_dict['pr_id'])
203
203
204 # Only proceed if we got a pull request and if acl repo name from
204 # Only proceed if we got a pull request and if acl repo name from
205 # URL equals the target repo name of the pull request.
205 # URL equals the target repo name of the pull request.
206 if pull_request and \
206 if pull_request and \
207 (acl_repo_name == pull_request.target_repo.repo_name):
207 (acl_repo_name == pull_request.target_repo.repo_name):
208 repo_id = pull_request.target_repo.repo_id
208 repo_id = pull_request.target_repo.repo_id
209 # Get file system path to shadow repository.
209 # Get file system path to shadow repository.
210 workspace_id = PullRequestModel()._workspace_id(pull_request)
210 workspace_id = PullRequestModel()._workspace_id(pull_request)
211 target_vcs = pull_request.target_repo.scm_instance()
211 target_vcs = pull_request.target_repo.scm_instance()
212 vcs_repo_name = target_vcs._get_shadow_repository_path(
212 vcs_repo_name = target_vcs._get_shadow_repository_path(
213 repo_id, workspace_id)
213 repo_id, workspace_id)
214
214
215 # Store names for later usage.
215 # Store names for later usage.
216 self.vcs_repo_name = vcs_repo_name
216 self.vcs_repo_name = vcs_repo_name
217 self.acl_repo_name = acl_repo_name
217 self.acl_repo_name = acl_repo_name
218 self.is_shadow_repo = True
218 self.is_shadow_repo = True
219
219
220 log.debug('Setting all VCS repository names: %s', {
220 log.debug('Setting all VCS repository names: %s', {
221 'acl_repo_name': self.acl_repo_name,
221 'acl_repo_name': self.acl_repo_name,
222 'url_repo_name': self.url_repo_name,
222 'url_repo_name': self.url_repo_name,
223 'vcs_repo_name': self.vcs_repo_name,
223 'vcs_repo_name': self.vcs_repo_name,
224 })
224 })
225
225
226 @property
226 @property
227 def scm_app(self):
227 def scm_app(self):
228 custom_implementation = self.config['vcs.scm_app_implementation']
228 custom_implementation = self.config['vcs.scm_app_implementation']
229 if custom_implementation == 'http':
229 if custom_implementation == 'http':
230 log.info('Using HTTP implementation of scm app.')
230 log.info('Using HTTP implementation of scm app.')
231 scm_app_impl = scm_app_http
231 scm_app_impl = scm_app_http
232 else:
232 else:
233 log.info('Using custom implementation of scm_app: "{}"'.format(
233 log.info('Using custom implementation of scm_app: "{}"'.format(
234 custom_implementation))
234 custom_implementation))
235 scm_app_impl = importlib.import_module(custom_implementation)
235 scm_app_impl = importlib.import_module(custom_implementation)
236 return scm_app_impl
236 return scm_app_impl
237
237
238 def _get_by_id(self, repo_name):
238 def _get_by_id(self, repo_name):
239 """
239 """
240 Gets a special pattern _<ID> from clone url and tries to replace it
240 Gets a special pattern _<ID> from clone url and tries to replace it
241 with a repository_name for support of _<ID> non changeable urls
241 with a repository_name for support of _<ID> non changeable urls
242 """
242 """
243
243
244 data = repo_name.split('/')
244 data = repo_name.split('/')
245 if len(data) >= 2:
245 if len(data) >= 2:
246 from rhodecode.model.repo import RepoModel
246 from rhodecode.model.repo import RepoModel
247 by_id_match = RepoModel().get_repo_by_id(repo_name)
247 by_id_match = RepoModel().get_repo_by_id(repo_name)
248 if by_id_match:
248 if by_id_match:
249 data[1] = by_id_match.repo_name
249 data[1] = by_id_match.repo_name
250
250
251 return safe_str('/'.join(data))
251 return safe_str('/'.join(data))
252
252
253 def _invalidate_cache(self, repo_name):
253 def _invalidate_cache(self, repo_name):
254 """
254 """
255 Set's cache for this repository for invalidation on next access
255 Set's cache for this repository for invalidation on next access
256
256
257 :param repo_name: full repo name, also a cache key
257 :param repo_name: full repo name, also a cache key
258 """
258 """
259 ScmModel().mark_for_invalidation(repo_name)
259 ScmModel().mark_for_invalidation(repo_name)
260
260
261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 db_repo = Repository.get_by_repo_name(repo_name)
262 db_repo = Repository.get_by_repo_name(repo_name)
263 if not db_repo:
263 if not db_repo:
264 log.debug('Repository `%s` not found inside the database.',
264 log.debug('Repository `%s` not found inside the database.',
265 repo_name)
265 repo_name)
266 return False
266 return False
267
267
268 if db_repo.repo_type != scm_type:
268 if db_repo.repo_type != scm_type:
269 log.warning(
269 log.warning(
270 'Repository `%s` have incorrect scm_type, expected %s got %s',
270 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 repo_name, db_repo.repo_type, scm_type)
271 repo_name, db_repo.repo_type, scm_type)
272 return False
272 return False
273
273
274 config = db_repo._config
274 config = db_repo._config
275 config.set('extensions', 'largefiles', '')
275 config.set('extensions', 'largefiles', '')
276 return is_valid_repo(
276 return is_valid_repo(
277 repo_name, base_path,
277 repo_name, base_path,
278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279
279
280 def valid_and_active_user(self, user):
280 def valid_and_active_user(self, user):
281 """
281 """
282 Checks if that user is not empty, and if it's actually object it checks
282 Checks if that user is not empty, and if it's actually object it checks
283 if he's active.
283 if he's active.
284
284
285 :param user: user object or None
285 :param user: user object or None
286 :return: boolean
286 :return: boolean
287 """
287 """
288 if user is None:
288 if user is None:
289 return False
289 return False
290
290
291 elif user.active:
291 elif user.active:
292 return True
292 return True
293
293
294 return False
294 return False
295
295
296 @property
296 @property
297 def is_shadow_repo_dir(self):
297 def is_shadow_repo_dir(self):
298 return os.path.isdir(self.vcs_repo_name)
298 return os.path.isdir(self.vcs_repo_name)
299
299
300 def _check_permission(self, action, user, repo_name, ip_addr=None,
300 def _check_permission(self, action, user, repo_name, ip_addr=None,
301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 """
302 """
303 Checks permissions using action (push/pull) user and repository
303 Checks permissions using action (push/pull) user and repository
304 name. If plugin_cache and ttl is set it will use the plugin which
304 name. If plugin_cache and ttl is set it will use the plugin which
305 authenticated the user to store the cached permissions result for N
305 authenticated the user to store the cached permissions result for N
306 amount of seconds as in cache_ttl
306 amount of seconds as in cache_ttl
307
307
308 :param action: push or pull action
308 :param action: push or pull action
309 :param user: user instance
309 :param user: user instance
310 :param repo_name: repository name
310 :param repo_name: repository name
311 """
311 """
312
312
313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 plugin_id, plugin_cache_active, cache_ttl)
314 plugin_id, plugin_cache_active, cache_ttl)
315
315
316 user_id = user.user_id
316 user_id = user.user_id
317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319
319
320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 expiration_time=cache_ttl,
321 expiration_time=cache_ttl,
322 condition=plugin_cache_active)
322 condition=plugin_cache_active)
323 def compute_perm_vcs(
323 def compute_perm_vcs(
324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325
325
326 log.debug('auth: calculating permission access now...')
326 log.debug('auth: calculating permission access now...')
327 # check IP
327 # check IP
328 inherit = user.inherit_default_permissions
328 inherit = user.inherit_default_permissions
329 ip_allowed = AuthUser.check_ip_allowed(
329 ip_allowed = AuthUser.check_ip_allowed(
330 user_id, ip_addr, inherit_from_default=inherit)
330 user_id, ip_addr, inherit_from_default=inherit)
331 if ip_allowed:
331 if ip_allowed:
332 log.info('Access for IP:%s allowed', ip_addr)
332 log.info('Access for IP:%s allowed', ip_addr)
333 else:
333 else:
334 return False
334 return False
335
335
336 if action == 'push':
336 if action == 'push':
337 perms = ('repository.write', 'repository.admin')
337 perms = ('repository.write', 'repository.admin')
338 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
338 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
339 return False
339 return False
340
340
341 else:
341 else:
342 # any other action need at least read permission
342 # any other action need at least read permission
343 perms = (
343 perms = (
344 'repository.read', 'repository.write', 'repository.admin')
344 'repository.read', 'repository.write', 'repository.admin')
345 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
345 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
346 return False
346 return False
347
347
348 return True
348 return True
349
349
350 start = time.time()
350 start = time.time()
351 log.debug('Running plugin `%s` permissions check', plugin_id)
351 log.debug('Running plugin `%s` permissions check', plugin_id)
352
352
353 # for environ based auth, password can be empty, but then the validation is
353 # for environ based auth, password can be empty, but then the validation is
354 # on the server that fills in the env data needed for authentication
354 # on the server that fills in the env data needed for authentication
355 perm_result = compute_perm_vcs(
355 perm_result = compute_perm_vcs(
356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357
357
358 auth_time = time.time() - start
358 auth_time = time.time() - start
359 log.debug('Permissions for plugin `%s` completed in %.3fs, '
359 log.debug('Permissions for plugin `%s` completed in %.3fs, '
360 'expiration time of fetched cache %.1fs.',
360 'expiration time of fetched cache %.1fs.',
361 plugin_id, auth_time, cache_ttl)
361 plugin_id, auth_time, cache_ttl)
362
362
363 return perm_result
363 return perm_result
364
364
365 def _check_ssl(self, environ, start_response):
365 def _check_ssl(self, environ, start_response):
366 """
366 """
367 Checks the SSL check flag and returns False if SSL is not present
367 Checks the SSL check flag and returns False if SSL is not present
368 and required True otherwise
368 and required True otherwise
369 """
369 """
370 org_proto = environ['wsgi._org_proto']
370 org_proto = environ['wsgi._org_proto']
371 # check if we have SSL required ! if not it's a bad request !
371 # check if we have SSL required ! if not it's a bad request !
372 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
372 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
373 if require_ssl and org_proto == 'http':
373 if require_ssl and org_proto == 'http':
374 log.debug(
374 log.debug(
375 'Bad request: detected protocol is `%s` and '
375 'Bad request: detected protocol is `%s` and '
376 'SSL/HTTPS is required.', org_proto)
376 'SSL/HTTPS is required.', org_proto)
377 return False
377 return False
378 return True
378 return True
379
379
380 def _get_default_cache_ttl(self):
380 def _get_default_cache_ttl(self):
381 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
381 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
382 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
382 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
383 plugin_settings = plugin.get_settings()
383 plugin_settings = plugin.get_settings()
384 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
384 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
385 plugin_settings) or (False, 0)
385 plugin_settings) or (False, 0)
386 return plugin_cache_active, cache_ttl
386 return plugin_cache_active, cache_ttl
387
387
388 def __call__(self, environ, start_response):
388 def __call__(self, environ, start_response):
389 try:
389 try:
390 return self._handle_request(environ, start_response)
390 return self._handle_request(environ, start_response)
391 except Exception:
391 except Exception:
392 log.exception("Exception while handling request")
392 log.exception("Exception while handling request")
393 appenlight.track_exception(environ)
393 appenlight.track_exception(environ)
394 return HTTPInternalServerError()(environ, start_response)
394 return HTTPInternalServerError()(environ, start_response)
395 finally:
395 finally:
396 meta.Session.remove()
396 meta.Session.remove()
397
397
398 def _handle_request(self, environ, start_response):
398 def _handle_request(self, environ, start_response):
399
399
400 if not self._check_ssl(environ, start_response):
400 if not self._check_ssl(environ, start_response):
401 reason = ('SSL required, while RhodeCode was unable '
401 reason = ('SSL required, while RhodeCode was unable '
402 'to detect this as SSL request')
402 'to detect this as SSL request')
403 log.debug('User not allowed to proceed, %s', reason)
403 log.debug('User not allowed to proceed, %s', reason)
404 return HTTPNotAcceptable(reason)(environ, start_response)
404 return HTTPNotAcceptable(reason)(environ, start_response)
405
405
406 if not self.url_repo_name:
406 if not self.url_repo_name:
407 log.warning('Repository name is empty: %s', self.url_repo_name)
407 log.warning('Repository name is empty: %s', self.url_repo_name)
408 # failed to get repo name, we fail now
408 # failed to get repo name, we fail now
409 return HTTPNotFound()(environ, start_response)
409 return HTTPNotFound()(environ, start_response)
410 log.debug('Extracted repo name is %s', self.url_repo_name)
410 log.debug('Extracted repo name is %s', self.url_repo_name)
411
411
412 ip_addr = get_ip_addr(environ)
412 ip_addr = get_ip_addr(environ)
413 user_agent = get_user_agent(environ)
413 user_agent = get_user_agent(environ)
414 username = None
414 username = None
415
415
416 # skip passing error to error controller
416 # skip passing error to error controller
417 environ['pylons.status_code_redirect'] = True
417 environ['pylons.status_code_redirect'] = True
418
418
419 # ======================================================================
419 # ======================================================================
420 # GET ACTION PULL or PUSH
420 # GET ACTION PULL or PUSH
421 # ======================================================================
421 # ======================================================================
422 action = self._get_action(environ)
422 action = self._get_action(environ)
423
423
424 # ======================================================================
424 # ======================================================================
425 # Check if this is a request to a shadow repository of a pull request.
425 # Check if this is a request to a shadow repository of a pull request.
426 # In this case only pull action is allowed.
426 # In this case only pull action is allowed.
427 # ======================================================================
427 # ======================================================================
428 if self.is_shadow_repo and action != 'pull':
428 if self.is_shadow_repo and action != 'pull':
429 reason = 'Only pull action is allowed for shadow repositories.'
429 reason = 'Only pull action is allowed for shadow repositories.'
430 log.debug('User not allowed to proceed, %s', reason)
430 log.debug('User not allowed to proceed, %s', reason)
431 return HTTPNotAcceptable(reason)(environ, start_response)
431 return HTTPNotAcceptable(reason)(environ, start_response)
432
432
433 # Check if the shadow repo actually exists, in case someone refers
433 # Check if the shadow repo actually exists, in case someone refers
434 # to it, and it has been deleted because of successful merge.
434 # to it, and it has been deleted because of successful merge.
435 if self.is_shadow_repo and not self.is_shadow_repo_dir:
435 if self.is_shadow_repo and not self.is_shadow_repo_dir:
436 log.debug(
436 log.debug(
437 'Shadow repo detected, and shadow repo dir `%s` is missing',
437 'Shadow repo detected, and shadow repo dir `%s` is missing',
438 self.is_shadow_repo_dir)
438 self.is_shadow_repo_dir)
439 return HTTPNotFound()(environ, start_response)
439 return HTTPNotFound()(environ, start_response)
440
440
441 # ======================================================================
441 # ======================================================================
442 # CHECK ANONYMOUS PERMISSION
442 # CHECK ANONYMOUS PERMISSION
443 # ======================================================================
443 # ======================================================================
444 if action in ['pull', 'push']:
444 if action in ['pull', 'push']:
445 anonymous_user = User.get_default_user()
445 anonymous_user = User.get_default_user()
446 username = anonymous_user.username
446 username = anonymous_user.username
447 if anonymous_user.active:
447 if anonymous_user.active:
448 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
448 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
449 # ONLY check permissions if the user is activated
449 # ONLY check permissions if the user is activated
450 anonymous_perm = self._check_permission(
450 anonymous_perm = self._check_permission(
451 action, anonymous_user, self.acl_repo_name, ip_addr,
451 action, anonymous_user, self.acl_repo_name, ip_addr,
452 plugin_id='anonymous_access',
452 plugin_id='anonymous_access',
453 plugin_cache_active=plugin_cache_active,
453 plugin_cache_active=plugin_cache_active,
454 cache_ttl=cache_ttl,
454 cache_ttl=cache_ttl,
455 )
455 )
456 else:
456 else:
457 anonymous_perm = False
457 anonymous_perm = False
458
458
459 if not anonymous_user.active or not anonymous_perm:
459 if not anonymous_user.active or not anonymous_perm:
460 if not anonymous_user.active:
460 if not anonymous_user.active:
461 log.debug('Anonymous access is disabled, running '
461 log.debug('Anonymous access is disabled, running '
462 'authentication')
462 'authentication')
463
463
464 if not anonymous_perm:
464 if not anonymous_perm:
465 log.debug('Not enough credentials to access this '
465 log.debug('Not enough credentials to access this '
466 'repository as anonymous user')
466 'repository as anonymous user')
467
467
468 username = None
468 username = None
469 # ==============================================================
469 # ==============================================================
470 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
470 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
471 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
471 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
472 # ==============================================================
472 # ==============================================================
473
473
474 # try to auth based on environ, container auth methods
474 # try to auth based on environ, container auth methods
475 log.debug('Running PRE-AUTH for container based authentication')
475 log.debug('Running PRE-AUTH for container based authentication')
476 pre_auth = authenticate(
476 pre_auth = authenticate(
477 '', '', environ, VCS_TYPE, registry=self.registry,
477 '', '', environ, VCS_TYPE, registry=self.registry,
478 acl_repo_name=self.acl_repo_name)
478 acl_repo_name=self.acl_repo_name)
479 if pre_auth and pre_auth.get('username'):
479 if pre_auth and pre_auth.get('username'):
480 username = pre_auth['username']
480 username = pre_auth['username']
481 log.debug('PRE-AUTH got %s as username', username)
481 log.debug('PRE-AUTH got %s as username', username)
482 if pre_auth:
482 if pre_auth:
483 log.debug('PRE-AUTH successful from %s',
483 log.debug('PRE-AUTH successful from %s',
484 pre_auth.get('auth_data', {}).get('_plugin'))
484 pre_auth.get('auth_data', {}).get('_plugin'))
485
485
486 # If not authenticated by the container, running basic auth
486 # If not authenticated by the container, running basic auth
487 # before inject the calling repo_name for special scope checks
487 # before inject the calling repo_name for special scope checks
488 self.authenticate.acl_repo_name = self.acl_repo_name
488 self.authenticate.acl_repo_name = self.acl_repo_name
489
489
490 plugin_cache_active, cache_ttl = False, 0
490 plugin_cache_active, cache_ttl = False, 0
491 plugin = None
491 plugin = None
492 if not username:
492 if not username:
493 self.authenticate.realm = self.authenticate.get_rc_realm()
493 self.authenticate.realm = self.authenticate.get_rc_realm()
494
494
495 try:
495 try:
496 auth_result = self.authenticate(environ)
496 auth_result = self.authenticate(environ)
497 except (UserCreationError, NotAllowedToCreateUserError) as e:
497 except (UserCreationError, NotAllowedToCreateUserError) as e:
498 log.error(e)
498 log.error(e)
499 reason = safe_str(e)
499 reason = safe_str(e)
500 return HTTPNotAcceptable(reason)(environ, start_response)
500 return HTTPNotAcceptable(reason)(environ, start_response)
501
501
502 if isinstance(auth_result, dict):
502 if isinstance(auth_result, dict):
503 AUTH_TYPE.update(environ, 'basic')
503 AUTH_TYPE.update(environ, 'basic')
504 REMOTE_USER.update(environ, auth_result['username'])
504 REMOTE_USER.update(environ, auth_result['username'])
505 username = auth_result['username']
505 username = auth_result['username']
506 plugin = auth_result.get('auth_data', {}).get('_plugin')
506 plugin = auth_result.get('auth_data', {}).get('_plugin')
507 log.info(
507 log.info(
508 'MAIN-AUTH successful for user `%s` from %s plugin',
508 'MAIN-AUTH successful for user `%s` from %s plugin',
509 username, plugin)
509 username, plugin)
510
510
511 plugin_cache_active, cache_ttl = auth_result.get(
511 plugin_cache_active, cache_ttl = auth_result.get(
512 'auth_data', {}).get('_ttl_cache') or (False, 0)
512 'auth_data', {}).get('_ttl_cache') or (False, 0)
513 else:
513 else:
514 return auth_result.wsgi_application(
514 return auth_result.wsgi_application(
515 environ, start_response)
515 environ, start_response)
516
516
517 # ==============================================================
517 # ==============================================================
518 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
518 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
519 # ==============================================================
519 # ==============================================================
520 user = User.get_by_username(username)
520 user = User.get_by_username(username)
521 if not self.valid_and_active_user(user):
521 if not self.valid_and_active_user(user):
522 return HTTPForbidden()(environ, start_response)
522 return HTTPForbidden()(environ, start_response)
523 username = user.username
523 username = user.username
524 user_id = user.user_id
524 user_id = user.user_id
525
525
526 # check user attributes for password change flag
526 # check user attributes for password change flag
527 user_obj = user
527 user_obj = user
528 if user_obj and user_obj.username != User.DEFAULT_USER and \
528 if user_obj and user_obj.username != User.DEFAULT_USER and \
529 user_obj.user_data.get('force_password_change'):
529 user_obj.user_data.get('force_password_change'):
530 reason = 'password change required'
530 reason = 'password change required'
531 log.debug('User not allowed to authenticate, %s', reason)
531 log.debug('User not allowed to authenticate, %s', reason)
532 return HTTPNotAcceptable(reason)(environ, start_response)
532 return HTTPNotAcceptable(reason)(environ, start_response)
533
533
534 # check permissions for this repository
534 # check permissions for this repository
535 perm = self._check_permission(
535 perm = self._check_permission(
536 action, user, self.acl_repo_name, ip_addr,
536 action, user, self.acl_repo_name, ip_addr,
537 plugin, plugin_cache_active, cache_ttl)
537 plugin, plugin_cache_active, cache_ttl)
538 if not perm:
538 if not perm:
539 return HTTPForbidden()(environ, start_response)
539 return HTTPForbidden()(environ, start_response)
540 environ['rc_auth_user_id'] = user_id
540 environ['rc_auth_user_id'] = user_id
541
541
542 # extras are injected into UI object and later available
542 # extras are injected into UI object and later available
543 # in hooks executed by RhodeCode
543 # in hooks executed by RhodeCode
544 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
544 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
545 extras = vcs_operation_context(
545 extras = vcs_operation_context(
546 environ, repo_name=self.acl_repo_name, username=username,
546 environ, repo_name=self.acl_repo_name, username=username,
547 action=action, scm=self.SCM, check_locking=check_locking,
547 action=action, scm=self.SCM, check_locking=check_locking,
548 is_shadow_repo=self.is_shadow_repo
548 is_shadow_repo=self.is_shadow_repo
549 )
549 )
550
550
551 # ======================================================================
551 # ======================================================================
552 # REQUEST HANDLING
552 # REQUEST HANDLING
553 # ======================================================================
553 # ======================================================================
554 repo_path = os.path.join(
554 repo_path = os.path.join(
555 safe_str(self.base_path), safe_str(self.vcs_repo_name))
555 safe_str(self.base_path), safe_str(self.vcs_repo_name))
556 log.debug('Repository path is %s', repo_path)
556 log.debug('Repository path is %s', repo_path)
557
557
558 fix_PATH()
558 fix_PATH()
559
559
560 log.info(
560 log.info(
561 '%s action on %s repo "%s" by "%s" from %s %s',
561 '%s action on %s repo "%s" by "%s" from %s %s',
562 action, self.SCM, safe_str(self.url_repo_name),
562 action, self.SCM, safe_str(self.url_repo_name),
563 safe_str(username), ip_addr, user_agent)
563 safe_str(username), ip_addr, user_agent)
564
564
565 return self._generate_vcs_response(
565 return self._generate_vcs_response(
566 environ, start_response, repo_path, extras, action)
566 environ, start_response, repo_path, extras, action)
567
567
568 @initialize_generator
568 @initialize_generator
569 def _generate_vcs_response(
569 def _generate_vcs_response(
570 self, environ, start_response, repo_path, extras, action):
570 self, environ, start_response, repo_path, extras, action):
571 """
571 """
572 Returns a generator for the response content.
572 Returns a generator for the response content.
573
573
574 This method is implemented as a generator, so that it can trigger
574 This method is implemented as a generator, so that it can trigger
575 the cache validation after all content sent back to the client. It
575 the cache validation after all content sent back to the client. It
576 also handles the locking exceptions which will be triggered when
576 also handles the locking exceptions which will be triggered when
577 the first chunk is produced by the underlying WSGI application.
577 the first chunk is produced by the underlying WSGI application.
578 """
578 """
579 txn_id = ''
579 txn_id = ''
580 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
580 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
581 # case for SVN, we want to re-use the callback daemon port
581 # case for SVN, we want to re-use the callback daemon port
582 # so we use the txn_id, for this we peek the body, and still save
582 # so we use the txn_id, for this we peek the body, and still save
583 # it as wsgi.input
583 # it as wsgi.input
584 data = environ['wsgi.input'].read()
584 data = environ['wsgi.input'].read()
585 environ['wsgi.input'] = StringIO(data)
585 environ['wsgi.input'] = StringIO(data)
586 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
586 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
587
587
588 callback_daemon, extras = self._prepare_callback_daemon(
588 callback_daemon, extras = self._prepare_callback_daemon(
589 extras, environ, action, txn_id=txn_id)
589 extras, environ, action, txn_id=txn_id)
590 log.debug('HOOKS extras is %s', extras)
590 log.debug('HOOKS extras is %s', extras)
591
591
592 config = self._create_config(extras, self.acl_repo_name)
592 config = self._create_config(extras, self.acl_repo_name)
593 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
593 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
594 with callback_daemon:
594 with callback_daemon:
595 app.rc_extras = extras
595 app.rc_extras = extras
596
596
597 try:
597 try:
598 response = app(environ, start_response)
598 response = app(environ, start_response)
599 finally:
599 finally:
600 # This statement works together with the decorator
600 # This statement works together with the decorator
601 # "initialize_generator" above. The decorator ensures that
601 # "initialize_generator" above. The decorator ensures that
602 # we hit the first yield statement before the generator is
602 # we hit the first yield statement before the generator is
603 # returned back to the WSGI server. This is needed to
603 # returned back to the WSGI server. This is needed to
604 # ensure that the call to "app" above triggers the
604 # ensure that the call to "app" above triggers the
605 # needed callback to "start_response" before the
605 # needed callback to "start_response" before the
606 # generator is actually used.
606 # generator is actually used.
607 yield "__init__"
607 yield "__init__"
608
608
609 # iter content
609 # iter content
610 for chunk in response:
610 for chunk in response:
611 yield chunk
611 yield chunk
612
612
613 try:
613 try:
614 # invalidate cache on push
614 # invalidate cache on push
615 if action == 'push':
615 if action == 'push':
616 self._invalidate_cache(self.url_repo_name)
616 self._invalidate_cache(self.url_repo_name)
617 finally:
617 finally:
618 meta.Session.remove()
618 meta.Session.remove()
619
619
620 def _get_repository_name(self, environ):
620 def _get_repository_name(self, environ):
621 """Get repository name out of the environmnent
621 """Get repository name out of the environmnent
622
622
623 :param environ: WSGI environment
623 :param environ: WSGI environment
624 """
624 """
625 raise NotImplementedError()
625 raise NotImplementedError()
626
626
627 def _get_action(self, environ):
627 def _get_action(self, environ):
628 """Map request commands into a pull or push command.
628 """Map request commands into a pull or push command.
629
629
630 :param environ: WSGI environment
630 :param environ: WSGI environment
631 """
631 """
632 raise NotImplementedError()
632 raise NotImplementedError()
633
633
634 def _create_wsgi_app(self, repo_path, repo_name, config):
634 def _create_wsgi_app(self, repo_path, repo_name, config):
635 """Return the WSGI app that will finally handle the request."""
635 """Return the WSGI app that will finally handle the request."""
636 raise NotImplementedError()
636 raise NotImplementedError()
637
637
638 def _create_config(self, extras, repo_name):
638 def _create_config(self, extras, repo_name):
639 """Create a safe config representation."""
639 """Create a safe config representation."""
640 raise NotImplementedError()
640 raise NotImplementedError()
641
641
642 def _should_use_callback_daemon(self, extras, environ, action):
642 def _should_use_callback_daemon(self, extras, environ, action):
643 return True
643 return True
644
644
645 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
645 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
646 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
646 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
647 if not self._should_use_callback_daemon(extras, environ, action):
647 if not self._should_use_callback_daemon(extras, environ, action):
648 # disable callback daemon for actions that don't require it
648 # disable callback daemon for actions that don't require it
649 direct_calls = True
649 direct_calls = True
650
650
651 return prepare_callback_daemon(
651 return prepare_callback_daemon(
652 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
652 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
653 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
653 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
654
654
655
655
656 def _should_check_locking(query_string):
656 def _should_check_locking(query_string):
657 # this is kind of hacky, but due to how mercurial handles client-server
657 # this is kind of hacky, but due to how mercurial handles client-server
658 # server see all operation on commit; bookmarks, phases and
658 # server see all operation on commit; bookmarks, phases and
659 # obsolescence marker in different transaction, we don't want to check
659 # obsolescence marker in different transaction, we don't want to check
660 # locking on those
660 # locking on those
661 return query_string not in ['cmd=listkeys']
661 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now