##// END OF EJS Templates
core: always attach pyramid context into request...
marcink -
r1896:936e31fc default
parent child Browse files
Show More
@@ -1,542 +1,542 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 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 inspect
21 import inspect
22 import itertools
22 import itertools
23 import logging
23 import logging
24 import types
24 import types
25 import fnmatch
25 import fnmatch
26
26
27 import decorator
27 import decorator
28 import venusian
28 import venusian
29 from collections import OrderedDict
29 from collections import OrderedDict
30
30
31 from pyramid.exceptions import ConfigurationError
31 from pyramid.exceptions import ConfigurationError
32 from pyramid.renderers import render
32 from pyramid.renderers import render
33 from pyramid.response import Response
33 from pyramid.response import Response
34 from pyramid.httpexceptions import HTTPNotFound
34 from pyramid.httpexceptions import HTTPNotFound
35
35
36 from rhodecode.api.exc import (
36 from rhodecode.api.exc import (
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 from rhodecode.apps._base import TemplateArgs
38 from rhodecode.apps._base import TemplateArgs
39 from rhodecode.lib.auth import AuthUser
39 from rhodecode.lib.auth import AuthUser
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.lib.plugins.utils import get_plugin_settings
43 from rhodecode.lib.plugins.utils import get_plugin_settings
44 from rhodecode.model.db import User, UserApiKeys
44 from rhodecode.model.db import User, UserApiKeys
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 DEFAULT_RENDERER = 'jsonrpc_renderer'
49 DEFAULT_URL = '/_admin/apiv2'
49 DEFAULT_URL = '/_admin/apiv2'
50
50
51
51
52 def find_methods(jsonrpc_methods, pattern):
52 def find_methods(jsonrpc_methods, pattern):
53 matches = OrderedDict()
53 matches = OrderedDict()
54 if not isinstance(pattern, (list, tuple)):
54 if not isinstance(pattern, (list, tuple)):
55 pattern = [pattern]
55 pattern = [pattern]
56
56
57 for single_pattern in pattern:
57 for single_pattern in pattern:
58 for method_name, method in jsonrpc_methods.items():
58 for method_name, method in jsonrpc_methods.items():
59 if fnmatch.fnmatch(method_name, single_pattern):
59 if fnmatch.fnmatch(method_name, single_pattern):
60 matches[method_name] = method
60 matches[method_name] = method
61 return matches
61 return matches
62
62
63
63
64 class ExtJsonRenderer(object):
64 class ExtJsonRenderer(object):
65 """
65 """
66 Custom renderer that mkaes use of our ext_json lib
66 Custom renderer that mkaes use of our ext_json lib
67
67
68 """
68 """
69
69
70 def __init__(self, serializer=json.dumps, **kw):
70 def __init__(self, serializer=json.dumps, **kw):
71 """ Any keyword arguments will be passed to the ``serializer``
71 """ Any keyword arguments will be passed to the ``serializer``
72 function."""
72 function."""
73 self.serializer = serializer
73 self.serializer = serializer
74 self.kw = kw
74 self.kw = kw
75
75
76 def __call__(self, info):
76 def __call__(self, info):
77 """ Returns a plain JSON-encoded string with content-type
77 """ Returns a plain JSON-encoded string with content-type
78 ``application/json``. The content-type may be overridden by
78 ``application/json``. The content-type may be overridden by
79 setting ``request.response.content_type``."""
79 setting ``request.response.content_type``."""
80
80
81 def _render(value, system):
81 def _render(value, system):
82 request = system.get('request')
82 request = system.get('request')
83 if request is not None:
83 if request is not None:
84 response = request.response
84 response = request.response
85 ct = response.content_type
85 ct = response.content_type
86 if ct == response.default_content_type:
86 if ct == response.default_content_type:
87 response.content_type = 'application/json'
87 response.content_type = 'application/json'
88
88
89 return self.serializer(value, **self.kw)
89 return self.serializer(value, **self.kw)
90
90
91 return _render
91 return _render
92
92
93
93
94 def jsonrpc_response(request, result):
94 def jsonrpc_response(request, result):
95 rpc_id = getattr(request, 'rpc_id', None)
95 rpc_id = getattr(request, 'rpc_id', None)
96 response = request.response
96 response = request.response
97
97
98 # store content_type before render is called
98 # store content_type before render is called
99 ct = response.content_type
99 ct = response.content_type
100
100
101 ret_value = ''
101 ret_value = ''
102 if rpc_id:
102 if rpc_id:
103 ret_value = {
103 ret_value = {
104 'id': rpc_id,
104 'id': rpc_id,
105 'result': result,
105 'result': result,
106 'error': None,
106 'error': None,
107 }
107 }
108
108
109 # fetch deprecation warnings, and store it inside results
109 # fetch deprecation warnings, and store it inside results
110 deprecation = getattr(request, 'rpc_deprecation', None)
110 deprecation = getattr(request, 'rpc_deprecation', None)
111 if deprecation:
111 if deprecation:
112 ret_value['DEPRECATION_WARNING'] = deprecation
112 ret_value['DEPRECATION_WARNING'] = deprecation
113
113
114 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
114 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
115 response.body = safe_str(raw_body, response.charset)
115 response.body = safe_str(raw_body, response.charset)
116
116
117 if ct == response.default_content_type:
117 if ct == response.default_content_type:
118 response.content_type = 'application/json'
118 response.content_type = 'application/json'
119
119
120 return response
120 return response
121
121
122
122
123 def jsonrpc_error(request, message, retid=None, code=None):
123 def jsonrpc_error(request, message, retid=None, code=None):
124 """
124 """
125 Generate a Response object with a JSON-RPC error body
125 Generate a Response object with a JSON-RPC error body
126
126
127 :param code:
127 :param code:
128 :param retid:
128 :param retid:
129 :param message:
129 :param message:
130 """
130 """
131 err_dict = {'id': retid, 'result': None, 'error': message}
131 err_dict = {'id': retid, 'result': None, 'error': message}
132 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
132 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
133 return Response(
133 return Response(
134 body=body,
134 body=body,
135 status=code,
135 status=code,
136 content_type='application/json'
136 content_type='application/json'
137 )
137 )
138
138
139
139
140 def exception_view(exc, request):
140 def exception_view(exc, request):
141 rpc_id = getattr(request, 'rpc_id', None)
141 rpc_id = getattr(request, 'rpc_id', None)
142
142
143 fault_message = 'undefined error'
143 fault_message = 'undefined error'
144 if isinstance(exc, JSONRPCError):
144 if isinstance(exc, JSONRPCError):
145 fault_message = exc.message
145 fault_message = exc.message
146 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
146 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
147 elif isinstance(exc, JSONRPCValidationError):
147 elif isinstance(exc, JSONRPCValidationError):
148 colander_exc = exc.colander_exception
148 colander_exc = exc.colander_exception
149 # TODO(marcink): think maybe of nicer way to serialize errors ?
149 # TODO(marcink): think maybe of nicer way to serialize errors ?
150 fault_message = colander_exc.asdict()
150 fault_message = colander_exc.asdict()
151 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
151 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
152 elif isinstance(exc, JSONRPCForbidden):
152 elif isinstance(exc, JSONRPCForbidden):
153 fault_message = 'Access was denied to this resource.'
153 fault_message = 'Access was denied to this resource.'
154 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
154 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
155 elif isinstance(exc, HTTPNotFound):
155 elif isinstance(exc, HTTPNotFound):
156 method = request.rpc_method
156 method = request.rpc_method
157 log.debug('json-rpc method `%s` not found in list of '
157 log.debug('json-rpc method `%s` not found in list of '
158 'api calls: %s, rpc_id:%s',
158 'api calls: %s, rpc_id:%s',
159 method, request.registry.jsonrpc_methods.keys(), rpc_id)
159 method, request.registry.jsonrpc_methods.keys(), rpc_id)
160
160
161 similar = 'none'
161 similar = 'none'
162 try:
162 try:
163 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
163 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
164 similar_found = find_methods(
164 similar_found = find_methods(
165 request.registry.jsonrpc_methods, similar_paterns)
165 request.registry.jsonrpc_methods, similar_paterns)
166 similar = ', '.join(similar_found.keys()) or similar
166 similar = ', '.join(similar_found.keys()) or similar
167 except Exception:
167 except Exception:
168 # make the whole above block safe
168 # make the whole above block safe
169 pass
169 pass
170
170
171 fault_message = "No such method: {}. Similar methods: {}".format(
171 fault_message = "No such method: {}. Similar methods: {}".format(
172 method, similar)
172 method, similar)
173
173
174 return jsonrpc_error(request, fault_message, rpc_id)
174 return jsonrpc_error(request, fault_message, rpc_id)
175
175
176
176
177 def request_view(request):
177 def request_view(request):
178 """
178 """
179 Main request handling method. It handles all logic to call a specific
179 Main request handling method. It handles all logic to call a specific
180 exposed method
180 exposed method
181 """
181 """
182
182
183 # check if we can find this session using api_key, get_by_auth_token
183 # check if we can find this session using api_key, get_by_auth_token
184 # search not expired tokens only
184 # search not expired tokens only
185
185
186 try:
186 try:
187 api_user = User.get_by_auth_token(request.rpc_api_key)
187 api_user = User.get_by_auth_token(request.rpc_api_key)
188
188
189 if api_user is None:
189 if api_user is None:
190 return jsonrpc_error(
190 return jsonrpc_error(
191 request, retid=request.rpc_id, message='Invalid API KEY')
191 request, retid=request.rpc_id, message='Invalid API KEY')
192
192
193 if not api_user.active:
193 if not api_user.active:
194 return jsonrpc_error(
194 return jsonrpc_error(
195 request, retid=request.rpc_id,
195 request, retid=request.rpc_id,
196 message='Request from this user not allowed')
196 message='Request from this user not allowed')
197
197
198 # check if we are allowed to use this IP
198 # check if we are allowed to use this IP
199 auth_u = AuthUser(
199 auth_u = AuthUser(
200 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
200 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
201 if not auth_u.ip_allowed:
201 if not auth_u.ip_allowed:
202 return jsonrpc_error(
202 return jsonrpc_error(
203 request, retid=request.rpc_id,
203 request, retid=request.rpc_id,
204 message='Request from IP:%s not allowed' % (
204 message='Request from IP:%s not allowed' % (
205 request.rpc_ip_addr,))
205 request.rpc_ip_addr,))
206 else:
206 else:
207 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
207 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
208
208
209 # register our auth-user
209 # register our auth-user
210 request.rpc_user = auth_u
210 request.rpc_user = auth_u
211
211
212 # now check if token is valid for API
212 # now check if token is valid for API
213 auth_token = request.rpc_api_key
213 auth_token = request.rpc_api_key
214 token_match = api_user.authenticate_by_token(
214 token_match = api_user.authenticate_by_token(
215 auth_token, roles=[UserApiKeys.ROLE_API])
215 auth_token, roles=[UserApiKeys.ROLE_API])
216 invalid_token = not token_match
216 invalid_token = not token_match
217
217
218 log.debug('Checking if API KEY is valid with proper role')
218 log.debug('Checking if API KEY is valid with proper role')
219 if invalid_token:
219 if invalid_token:
220 return jsonrpc_error(
220 return jsonrpc_error(
221 request, retid=request.rpc_id,
221 request, retid=request.rpc_id,
222 message='API KEY invalid or, has bad role for an API call')
222 message='API KEY invalid or, has bad role for an API call')
223
223
224 except Exception:
224 except Exception:
225 log.exception('Error on API AUTH')
225 log.exception('Error on API AUTH')
226 return jsonrpc_error(
226 return jsonrpc_error(
227 request, retid=request.rpc_id, message='Invalid API KEY')
227 request, retid=request.rpc_id, message='Invalid API KEY')
228
228
229 method = request.rpc_method
229 method = request.rpc_method
230 func = request.registry.jsonrpc_methods[method]
230 func = request.registry.jsonrpc_methods[method]
231
231
232 # now that we have a method, add request._req_params to
232 # now that we have a method, add request._req_params to
233 # self.kargs and dispatch control to WGIController
233 # self.kargs and dispatch control to WGIController
234 argspec = inspect.getargspec(func)
234 argspec = inspect.getargspec(func)
235 arglist = argspec[0]
235 arglist = argspec[0]
236 defaults = map(type, argspec[3] or [])
236 defaults = map(type, argspec[3] or [])
237 default_empty = types.NotImplementedType
237 default_empty = types.NotImplementedType
238
238
239 # kw arguments required by this method
239 # kw arguments required by this method
240 func_kwargs = dict(itertools.izip_longest(
240 func_kwargs = dict(itertools.izip_longest(
241 reversed(arglist), reversed(defaults), fillvalue=default_empty))
241 reversed(arglist), reversed(defaults), fillvalue=default_empty))
242
242
243 # This attribute will need to be first param of a method that uses
243 # This attribute will need to be first param of a method that uses
244 # api_key, which is translated to instance of user at that name
244 # api_key, which is translated to instance of user at that name
245 user_var = 'apiuser'
245 user_var = 'apiuser'
246 request_var = 'request'
246 request_var = 'request'
247
247
248 for arg in [user_var, request_var]:
248 for arg in [user_var, request_var]:
249 if arg not in arglist:
249 if arg not in arglist:
250 return jsonrpc_error(
250 return jsonrpc_error(
251 request,
251 request,
252 retid=request.rpc_id,
252 retid=request.rpc_id,
253 message='This method [%s] does not support '
253 message='This method [%s] does not support '
254 'required parameter `%s`' % (func.__name__, arg))
254 'required parameter `%s`' % (func.__name__, arg))
255
255
256 # get our arglist and check if we provided them as args
256 # get our arglist and check if we provided them as args
257 for arg, default in func_kwargs.items():
257 for arg, default in func_kwargs.items():
258 if arg in [user_var, request_var]:
258 if arg in [user_var, request_var]:
259 # user_var and request_var are pre-hardcoded parameters and we
259 # user_var and request_var are pre-hardcoded parameters and we
260 # don't need to do any translation
260 # don't need to do any translation
261 continue
261 continue
262
262
263 # skip the required param check if it's default value is
263 # skip the required param check if it's default value is
264 # NotImplementedType (default_empty)
264 # NotImplementedType (default_empty)
265 if default == default_empty and arg not in request.rpc_params:
265 if default == default_empty and arg not in request.rpc_params:
266 return jsonrpc_error(
266 return jsonrpc_error(
267 request,
267 request,
268 retid=request.rpc_id,
268 retid=request.rpc_id,
269 message=('Missing non optional `%s` arg in JSON DATA' % arg)
269 message=('Missing non optional `%s` arg in JSON DATA' % arg)
270 )
270 )
271
271
272 # sanitize extra passed arguments
272 # sanitize extra passed arguments
273 for k in request.rpc_params.keys()[:]:
273 for k in request.rpc_params.keys()[:]:
274 if k not in func_kwargs:
274 if k not in func_kwargs:
275 del request.rpc_params[k]
275 del request.rpc_params[k]
276
276
277 call_params = request.rpc_params
277 call_params = request.rpc_params
278 call_params.update({
278 call_params.update({
279 'request': request,
279 'request': request,
280 'apiuser': auth_u
280 'apiuser': auth_u
281 })
281 })
282
282
283 # register some common functions for usage
283 # register some common functions for usage
284 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id,
284 attach_context_attributes(
285 attach_to_request=True)
285 TemplateArgs(), request, request.rpc_user.user_id)
286
286
287 try:
287 try:
288 ret_value = func(**call_params)
288 ret_value = func(**call_params)
289 return jsonrpc_response(request, ret_value)
289 return jsonrpc_response(request, ret_value)
290 except JSONRPCBaseError:
290 except JSONRPCBaseError:
291 raise
291 raise
292 except Exception:
292 except Exception:
293 log.exception('Unhandled exception occurred on api call: %s', func)
293 log.exception('Unhandled exception occurred on api call: %s', func)
294 return jsonrpc_error(request, retid=request.rpc_id,
294 return jsonrpc_error(request, retid=request.rpc_id,
295 message='Internal server error')
295 message='Internal server error')
296
296
297
297
298 def setup_request(request):
298 def setup_request(request):
299 """
299 """
300 Parse a JSON-RPC request body. It's used inside the predicates method
300 Parse a JSON-RPC request body. It's used inside the predicates method
301 to validate and bootstrap requests for usage in rpc calls.
301 to validate and bootstrap requests for usage in rpc calls.
302
302
303 We need to raise JSONRPCError here if we want to return some errors back to
303 We need to raise JSONRPCError here if we want to return some errors back to
304 user.
304 user.
305 """
305 """
306
306
307 log.debug('Executing setup request: %r', request)
307 log.debug('Executing setup request: %r', request)
308 request.rpc_ip_addr = get_ip_addr(request.environ)
308 request.rpc_ip_addr = get_ip_addr(request.environ)
309 # TODO(marcink): deprecate GET at some point
309 # TODO(marcink): deprecate GET at some point
310 if request.method not in ['POST', 'GET']:
310 if request.method not in ['POST', 'GET']:
311 log.debug('unsupported request method "%s"', request.method)
311 log.debug('unsupported request method "%s"', request.method)
312 raise JSONRPCError(
312 raise JSONRPCError(
313 'unsupported request method "%s". Please use POST' % request.method)
313 'unsupported request method "%s". Please use POST' % request.method)
314
314
315 if 'CONTENT_LENGTH' not in request.environ:
315 if 'CONTENT_LENGTH' not in request.environ:
316 log.debug("No Content-Length")
316 log.debug("No Content-Length")
317 raise JSONRPCError("Empty body, No Content-Length in request")
317 raise JSONRPCError("Empty body, No Content-Length in request")
318
318
319 else:
319 else:
320 length = request.environ['CONTENT_LENGTH']
320 length = request.environ['CONTENT_LENGTH']
321 log.debug('Content-Length: %s', length)
321 log.debug('Content-Length: %s', length)
322
322
323 if length == 0:
323 if length == 0:
324 log.debug("Content-Length is 0")
324 log.debug("Content-Length is 0")
325 raise JSONRPCError("Content-Length is 0")
325 raise JSONRPCError("Content-Length is 0")
326
326
327 raw_body = request.body
327 raw_body = request.body
328 try:
328 try:
329 json_body = json.loads(raw_body)
329 json_body = json.loads(raw_body)
330 except ValueError as e:
330 except ValueError as e:
331 # catch JSON errors Here
331 # catch JSON errors Here
332 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
332 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
333
333
334 request.rpc_id = json_body.get('id')
334 request.rpc_id = json_body.get('id')
335 request.rpc_method = json_body.get('method')
335 request.rpc_method = json_body.get('method')
336
336
337 # check required base parameters
337 # check required base parameters
338 try:
338 try:
339 api_key = json_body.get('api_key')
339 api_key = json_body.get('api_key')
340 if not api_key:
340 if not api_key:
341 api_key = json_body.get('auth_token')
341 api_key = json_body.get('auth_token')
342
342
343 if not api_key:
343 if not api_key:
344 raise KeyError('api_key or auth_token')
344 raise KeyError('api_key or auth_token')
345
345
346 # TODO(marcink): support passing in token in request header
346 # TODO(marcink): support passing in token in request header
347
347
348 request.rpc_api_key = api_key
348 request.rpc_api_key = api_key
349 request.rpc_id = json_body['id']
349 request.rpc_id = json_body['id']
350 request.rpc_method = json_body['method']
350 request.rpc_method = json_body['method']
351 request.rpc_params = json_body['args'] \
351 request.rpc_params = json_body['args'] \
352 if isinstance(json_body['args'], dict) else {}
352 if isinstance(json_body['args'], dict) else {}
353
353
354 log.debug(
354 log.debug(
355 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
355 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
356 except KeyError as e:
356 except KeyError as e:
357 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
357 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
358
358
359 log.debug('setup complete, now handling method:%s rpcid:%s',
359 log.debug('setup complete, now handling method:%s rpcid:%s',
360 request.rpc_method, request.rpc_id, )
360 request.rpc_method, request.rpc_id, )
361
361
362
362
363 class RoutePredicate(object):
363 class RoutePredicate(object):
364 def __init__(self, val, config):
364 def __init__(self, val, config):
365 self.val = val
365 self.val = val
366
366
367 def text(self):
367 def text(self):
368 return 'jsonrpc route = %s' % self.val
368 return 'jsonrpc route = %s' % self.val
369
369
370 phash = text
370 phash = text
371
371
372 def __call__(self, info, request):
372 def __call__(self, info, request):
373 if self.val:
373 if self.val:
374 # potentially setup and bootstrap our call
374 # potentially setup and bootstrap our call
375 setup_request(request)
375 setup_request(request)
376
376
377 # Always return True so that even if it isn't a valid RPC it
377 # Always return True so that even if it isn't a valid RPC it
378 # will fall through to the underlaying handlers like notfound_view
378 # will fall through to the underlaying handlers like notfound_view
379 return True
379 return True
380
380
381
381
382 class NotFoundPredicate(object):
382 class NotFoundPredicate(object):
383 def __init__(self, val, config):
383 def __init__(self, val, config):
384 self.val = val
384 self.val = val
385 self.methods = config.registry.jsonrpc_methods
385 self.methods = config.registry.jsonrpc_methods
386
386
387 def text(self):
387 def text(self):
388 return 'jsonrpc method not found = {}.'.format(self.val)
388 return 'jsonrpc method not found = {}.'.format(self.val)
389
389
390 phash = text
390 phash = text
391
391
392 def __call__(self, info, request):
392 def __call__(self, info, request):
393 return hasattr(request, 'rpc_method')
393 return hasattr(request, 'rpc_method')
394
394
395
395
396 class MethodPredicate(object):
396 class MethodPredicate(object):
397 def __init__(self, val, config):
397 def __init__(self, val, config):
398 self.method = val
398 self.method = val
399
399
400 def text(self):
400 def text(self):
401 return 'jsonrpc method = %s' % self.method
401 return 'jsonrpc method = %s' % self.method
402
402
403 phash = text
403 phash = text
404
404
405 def __call__(self, context, request):
405 def __call__(self, context, request):
406 # we need to explicitly return False here, so pyramid doesn't try to
406 # we need to explicitly return False here, so pyramid doesn't try to
407 # execute our view directly. We need our main handler to execute things
407 # execute our view directly. We need our main handler to execute things
408 return getattr(request, 'rpc_method') == self.method
408 return getattr(request, 'rpc_method') == self.method
409
409
410
410
411 def add_jsonrpc_method(config, view, **kwargs):
411 def add_jsonrpc_method(config, view, **kwargs):
412 # pop the method name
412 # pop the method name
413 method = kwargs.pop('method', None)
413 method = kwargs.pop('method', None)
414
414
415 if method is None:
415 if method is None:
416 raise ConfigurationError(
416 raise ConfigurationError(
417 'Cannot register a JSON-RPC method without specifying the '
417 'Cannot register a JSON-RPC method without specifying the '
418 '"method"')
418 '"method"')
419
419
420 # we define custom predicate, to enable to detect conflicting methods,
420 # we define custom predicate, to enable to detect conflicting methods,
421 # those predicates are kind of "translation" from the decorator variables
421 # those predicates are kind of "translation" from the decorator variables
422 # to internal predicates names
422 # to internal predicates names
423
423
424 kwargs['jsonrpc_method'] = method
424 kwargs['jsonrpc_method'] = method
425
425
426 # register our view into global view store for validation
426 # register our view into global view store for validation
427 config.registry.jsonrpc_methods[method] = view
427 config.registry.jsonrpc_methods[method] = view
428
428
429 # we're using our main request_view handler, here, so each method
429 # we're using our main request_view handler, here, so each method
430 # has a unified handler for itself
430 # has a unified handler for itself
431 config.add_view(request_view, route_name='apiv2', **kwargs)
431 config.add_view(request_view, route_name='apiv2', **kwargs)
432
432
433
433
434 class jsonrpc_method(object):
434 class jsonrpc_method(object):
435 """
435 """
436 decorator that works similar to @add_view_config decorator,
436 decorator that works similar to @add_view_config decorator,
437 but tailored for our JSON RPC
437 but tailored for our JSON RPC
438 """
438 """
439
439
440 venusian = venusian # for testing injection
440 venusian = venusian # for testing injection
441
441
442 def __init__(self, method=None, **kwargs):
442 def __init__(self, method=None, **kwargs):
443 self.method = method
443 self.method = method
444 self.kwargs = kwargs
444 self.kwargs = kwargs
445
445
446 def __call__(self, wrapped):
446 def __call__(self, wrapped):
447 kwargs = self.kwargs.copy()
447 kwargs = self.kwargs.copy()
448 kwargs['method'] = self.method or wrapped.__name__
448 kwargs['method'] = self.method or wrapped.__name__
449 depth = kwargs.pop('_depth', 0)
449 depth = kwargs.pop('_depth', 0)
450
450
451 def callback(context, name, ob):
451 def callback(context, name, ob):
452 config = context.config.with_package(info.module)
452 config = context.config.with_package(info.module)
453 config.add_jsonrpc_method(view=ob, **kwargs)
453 config.add_jsonrpc_method(view=ob, **kwargs)
454
454
455 info = venusian.attach(wrapped, callback, category='pyramid',
455 info = venusian.attach(wrapped, callback, category='pyramid',
456 depth=depth + 1)
456 depth=depth + 1)
457 if info.scope == 'class':
457 if info.scope == 'class':
458 # ensure that attr is set if decorating a class method
458 # ensure that attr is set if decorating a class method
459 kwargs.setdefault('attr', wrapped.__name__)
459 kwargs.setdefault('attr', wrapped.__name__)
460
460
461 kwargs['_info'] = info.codeinfo # fbo action_method
461 kwargs['_info'] = info.codeinfo # fbo action_method
462 return wrapped
462 return wrapped
463
463
464
464
465 class jsonrpc_deprecated_method(object):
465 class jsonrpc_deprecated_method(object):
466 """
466 """
467 Marks method as deprecated, adds log.warning, and inject special key to
467 Marks method as deprecated, adds log.warning, and inject special key to
468 the request variable to mark method as deprecated.
468 the request variable to mark method as deprecated.
469 Also injects special docstring that extract_docs will catch to mark
469 Also injects special docstring that extract_docs will catch to mark
470 method as deprecated.
470 method as deprecated.
471
471
472 :param use_method: specify which method should be used instead of
472 :param use_method: specify which method should be used instead of
473 the decorated one
473 the decorated one
474
474
475 Use like::
475 Use like::
476
476
477 @jsonrpc_method()
477 @jsonrpc_method()
478 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
478 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
479 def old_func(request, apiuser, arg1, arg2):
479 def old_func(request, apiuser, arg1, arg2):
480 ...
480 ...
481 """
481 """
482
482
483 def __init__(self, use_method, deprecated_at_version):
483 def __init__(self, use_method, deprecated_at_version):
484 self.use_method = use_method
484 self.use_method = use_method
485 self.deprecated_at_version = deprecated_at_version
485 self.deprecated_at_version = deprecated_at_version
486 self.deprecated_msg = ''
486 self.deprecated_msg = ''
487
487
488 def __call__(self, func):
488 def __call__(self, func):
489 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
489 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
490 method=self.use_method)
490 method=self.use_method)
491
491
492 docstring = """\n
492 docstring = """\n
493 .. deprecated:: {version}
493 .. deprecated:: {version}
494
494
495 {deprecation_message}
495 {deprecation_message}
496
496
497 {original_docstring}
497 {original_docstring}
498 """
498 """
499 func.__doc__ = docstring.format(
499 func.__doc__ = docstring.format(
500 version=self.deprecated_at_version,
500 version=self.deprecated_at_version,
501 deprecation_message=self.deprecated_msg,
501 deprecation_message=self.deprecated_msg,
502 original_docstring=func.__doc__)
502 original_docstring=func.__doc__)
503 return decorator.decorator(self.__wrapper, func)
503 return decorator.decorator(self.__wrapper, func)
504
504
505 def __wrapper(self, func, *fargs, **fkwargs):
505 def __wrapper(self, func, *fargs, **fkwargs):
506 log.warning('DEPRECATED API CALL on function %s, please '
506 log.warning('DEPRECATED API CALL on function %s, please '
507 'use `%s` instead', func, self.use_method)
507 'use `%s` instead', func, self.use_method)
508 # alter function docstring to mark as deprecated, this is picked up
508 # alter function docstring to mark as deprecated, this is picked up
509 # via fabric file that generates API DOC.
509 # via fabric file that generates API DOC.
510 result = func(*fargs, **fkwargs)
510 result = func(*fargs, **fkwargs)
511
511
512 request = fargs[0]
512 request = fargs[0]
513 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
513 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
514 return result
514 return result
515
515
516
516
517 def includeme(config):
517 def includeme(config):
518 plugin_module = 'rhodecode.api'
518 plugin_module = 'rhodecode.api'
519 plugin_settings = get_plugin_settings(
519 plugin_settings = get_plugin_settings(
520 plugin_module, config.registry.settings)
520 plugin_module, config.registry.settings)
521
521
522 if not hasattr(config.registry, 'jsonrpc_methods'):
522 if not hasattr(config.registry, 'jsonrpc_methods'):
523 config.registry.jsonrpc_methods = OrderedDict()
523 config.registry.jsonrpc_methods = OrderedDict()
524
524
525 # match filter by given method only
525 # match filter by given method only
526 config.add_view_predicate('jsonrpc_method', MethodPredicate)
526 config.add_view_predicate('jsonrpc_method', MethodPredicate)
527
527
528 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
528 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
529 serializer=json.dumps, indent=4))
529 serializer=json.dumps, indent=4))
530 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
530 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
531
531
532 config.add_route_predicate(
532 config.add_route_predicate(
533 'jsonrpc_call', RoutePredicate)
533 'jsonrpc_call', RoutePredicate)
534
534
535 config.add_route(
535 config.add_route(
536 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
536 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
537
537
538 config.scan(plugin_module, ignore='rhodecode.api.tests')
538 config.scan(plugin_module, ignore='rhodecode.api.tests')
539 # register some exception handling view
539 # register some exception handling view
540 config.add_view(exception_view, context=JSONRPCBaseError)
540 config.add_view(exception_view, context=JSONRPCBaseError)
541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
542 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
542 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,605 +1,604 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import ipaddress
30 import ipaddress
31 import pyramid.threadlocal
31 import pyramid.threadlocal
32
32
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from pylons import config, tmpl_context as c, request, session, url
36 from pylons import config, tmpl_context as c, request, session, url
37 from pylons.controllers import WSGIController
37 from pylons.controllers import WSGIController
38 from pylons.controllers.util import redirect
38 from pylons.controllers.util import redirect
39 from pylons.i18n import translation
39 from pylons.i18n import translation
40 # marcink: don't remove this import
40 # marcink: don't remove this import
41 from pylons.templating import render_mako as render # noqa
41 from pylons.templating import render_mako as render # noqa
42 from pylons.i18n.translation import _
42 from pylons.i18n.translation import _
43 from webob.exc import HTTPFound
43 from webob.exc import HTTPFound
44
44
45
45
46 import rhodecode
46 import rhodecode
47 from rhodecode.authentication.base import VCS_TYPE
47 from rhodecode.authentication.base import VCS_TYPE
48 from rhodecode.lib import auth, utils2
48 from rhodecode.lib import auth, utils2
49 from rhodecode.lib import helpers as h
49 from rhodecode.lib import helpers as h
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 from rhodecode.lib.exceptions import UserCreationError
51 from rhodecode.lib.exceptions import UserCreationError
52 from rhodecode.lib.utils import (
52 from rhodecode.lib.utils import (
53 get_repo_slug, set_rhodecode_config, password_changed,
53 get_repo_slug, set_rhodecode_config, password_changed,
54 get_enabled_hook_classes)
54 get_enabled_hook_classes)
55 from rhodecode.lib.utils2 import (
55 from rhodecode.lib.utils2 import (
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 from rhodecode.model import meta
58 from rhodecode.model import meta
59 from rhodecode.model.db import Repository, User, ChangesetComment
59 from rhodecode.model.db import Repository, User, ChangesetComment
60 from rhodecode.model.notification import NotificationModel
60 from rhodecode.model.notification import NotificationModel
61 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63
63
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 def _filter_proxy(ip):
68 def _filter_proxy(ip):
69 """
69 """
70 Passed in IP addresses in HEADERS can be in a special format of multiple
70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 ips. Those comma separated IPs are passed from various proxies in the
71 ips. Those comma separated IPs are passed from various proxies in the
72 chain of request processing. The left-most being the original client.
72 chain of request processing. The left-most being the original client.
73 We only care about the first IP which came from the org. client.
73 We only care about the first IP which came from the org. client.
74
74
75 :param ip: ip string from headers
75 :param ip: ip string from headers
76 """
76 """
77 if ',' in ip:
77 if ',' in ip:
78 _ips = ip.split(',')
78 _ips = ip.split(',')
79 _first_ip = _ips[0].strip()
79 _first_ip = _ips[0].strip()
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 return _first_ip
81 return _first_ip
82 return ip
82 return ip
83
83
84
84
85 def _filter_port(ip):
85 def _filter_port(ip):
86 """
86 """
87 Removes a port from ip, there are 4 main cases to handle here.
87 Removes a port from ip, there are 4 main cases to handle here.
88 - ipv4 eg. 127.0.0.1
88 - ipv4 eg. 127.0.0.1
89 - ipv6 eg. ::1
89 - ipv6 eg. ::1
90 - ipv4+port eg. 127.0.0.1:8080
90 - ipv4+port eg. 127.0.0.1:8080
91 - ipv6+port eg. [::1]:8080
91 - ipv6+port eg. [::1]:8080
92
92
93 :param ip:
93 :param ip:
94 """
94 """
95 def is_ipv6(ip_addr):
95 def is_ipv6(ip_addr):
96 if hasattr(socket, 'inet_pton'):
96 if hasattr(socket, 'inet_pton'):
97 try:
97 try:
98 socket.inet_pton(socket.AF_INET6, ip_addr)
98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 except socket.error:
99 except socket.error:
100 return False
100 return False
101 else:
101 else:
102 # fallback to ipaddress
102 # fallback to ipaddress
103 try:
103 try:
104 ipaddress.IPv6Address(ip_addr)
104 ipaddress.IPv6Address(ip_addr)
105 except Exception:
105 except Exception:
106 return False
106 return False
107 return True
107 return True
108
108
109 if ':' not in ip: # must be ipv4 pure ip
109 if ':' not in ip: # must be ipv4 pure ip
110 return ip
110 return ip
111
111
112 if '[' in ip and ']' in ip: # ipv6 with port
112 if '[' in ip and ']' in ip: # ipv6 with port
113 return ip.split(']')[0][1:].lower()
113 return ip.split(']')[0][1:].lower()
114
114
115 # must be ipv6 or ipv4 with port
115 # must be ipv6 or ipv4 with port
116 if is_ipv6(ip):
116 if is_ipv6(ip):
117 return ip
117 return ip
118 else:
118 else:
119 ip, _port = ip.split(':')[:2] # means ipv4+port
119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 return ip
120 return ip
121
121
122
122
123 def get_ip_addr(environ):
123 def get_ip_addr(environ):
124 proxy_key = 'HTTP_X_REAL_IP'
124 proxy_key = 'HTTP_X_REAL_IP'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 def_key = 'REMOTE_ADDR'
126 def_key = 'REMOTE_ADDR'
127 _filters = lambda x: _filter_port(_filter_proxy(x))
127 _filters = lambda x: _filter_port(_filter_proxy(x))
128
128
129 ip = environ.get(proxy_key)
129 ip = environ.get(proxy_key)
130 if ip:
130 if ip:
131 return _filters(ip)
131 return _filters(ip)
132
132
133 ip = environ.get(proxy_key2)
133 ip = environ.get(proxy_key2)
134 if ip:
134 if ip:
135 return _filters(ip)
135 return _filters(ip)
136
136
137 ip = environ.get(def_key, '0.0.0.0')
137 ip = environ.get(def_key, '0.0.0.0')
138 return _filters(ip)
138 return _filters(ip)
139
139
140
140
141 def get_server_ip_addr(environ, log_errors=True):
141 def get_server_ip_addr(environ, log_errors=True):
142 hostname = environ.get('SERVER_NAME')
142 hostname = environ.get('SERVER_NAME')
143 try:
143 try:
144 return socket.gethostbyname(hostname)
144 return socket.gethostbyname(hostname)
145 except Exception as e:
145 except Exception as e:
146 if log_errors:
146 if log_errors:
147 # in some cases this lookup is not possible, and we don't want to
147 # in some cases this lookup is not possible, and we don't want to
148 # make it an exception in logs
148 # make it an exception in logs
149 log.exception('Could not retrieve server ip address: %s', e)
149 log.exception('Could not retrieve server ip address: %s', e)
150 return hostname
150 return hostname
151
151
152
152
153 def get_server_port(environ):
153 def get_server_port(environ):
154 return environ.get('SERVER_PORT')
154 return environ.get('SERVER_PORT')
155
155
156
156
157 def get_access_path(environ):
157 def get_access_path(environ):
158 path = environ.get('PATH_INFO')
158 path = environ.get('PATH_INFO')
159 org_req = environ.get('pylons.original_request')
159 org_req = environ.get('pylons.original_request')
160 if org_req:
160 if org_req:
161 path = org_req.environ.get('PATH_INFO')
161 path = org_req.environ.get('PATH_INFO')
162 return path
162 return path
163
163
164
164
165 def get_user_agent(environ):
165 def get_user_agent(environ):
166 return environ.get('HTTP_USER_AGENT')
166 return environ.get('HTTP_USER_AGENT')
167
167
168
168
169 def vcs_operation_context(
169 def vcs_operation_context(
170 environ, repo_name, username, action, scm, check_locking=True,
170 environ, repo_name, username, action, scm, check_locking=True,
171 is_shadow_repo=False):
171 is_shadow_repo=False):
172 """
172 """
173 Generate the context for a vcs operation, e.g. push or pull.
173 Generate the context for a vcs operation, e.g. push or pull.
174
174
175 This context is passed over the layers so that hooks triggered by the
175 This context is passed over the layers so that hooks triggered by the
176 vcs operation know details like the user, the user's IP address etc.
176 vcs operation know details like the user, the user's IP address etc.
177
177
178 :param check_locking: Allows to switch of the computation of the locking
178 :param check_locking: Allows to switch of the computation of the locking
179 data. This serves mainly the need of the simplevcs middleware to be
179 data. This serves mainly the need of the simplevcs middleware to be
180 able to disable this for certain operations.
180 able to disable this for certain operations.
181
181
182 """
182 """
183 # Tri-state value: False: unlock, None: nothing, True: lock
183 # Tri-state value: False: unlock, None: nothing, True: lock
184 make_lock = None
184 make_lock = None
185 locked_by = [None, None, None]
185 locked_by = [None, None, None]
186 is_anonymous = username == User.DEFAULT_USER
186 is_anonymous = username == User.DEFAULT_USER
187 if not is_anonymous and check_locking:
187 if not is_anonymous and check_locking:
188 log.debug('Checking locking on repository "%s"', repo_name)
188 log.debug('Checking locking on repository "%s"', repo_name)
189 user = User.get_by_username(username)
189 user = User.get_by_username(username)
190 repo = Repository.get_by_repo_name(repo_name)
190 repo = Repository.get_by_repo_name(repo_name)
191 make_lock, __, locked_by = repo.get_locking_state(
191 make_lock, __, locked_by = repo.get_locking_state(
192 action, user.user_id)
192 action, user.user_id)
193
193
194 settings_model = VcsSettingsModel(repo=repo_name)
194 settings_model = VcsSettingsModel(repo=repo_name)
195 ui_settings = settings_model.get_ui_settings()
195 ui_settings = settings_model.get_ui_settings()
196
196
197 extras = {
197 extras = {
198 'ip': get_ip_addr(environ),
198 'ip': get_ip_addr(environ),
199 'username': username,
199 'username': username,
200 'action': action,
200 'action': action,
201 'repository': repo_name,
201 'repository': repo_name,
202 'scm': scm,
202 'scm': scm,
203 'config': rhodecode.CONFIG['__file__'],
203 'config': rhodecode.CONFIG['__file__'],
204 'make_lock': make_lock,
204 'make_lock': make_lock,
205 'locked_by': locked_by,
205 'locked_by': locked_by,
206 'server_url': utils2.get_server_url(environ),
206 'server_url': utils2.get_server_url(environ),
207 'user_agent': get_user_agent(environ),
207 'user_agent': get_user_agent(environ),
208 'hooks': get_enabled_hook_classes(ui_settings),
208 'hooks': get_enabled_hook_classes(ui_settings),
209 'is_shadow_repo': is_shadow_repo,
209 'is_shadow_repo': is_shadow_repo,
210 }
210 }
211 return extras
211 return extras
212
212
213
213
214 class BasicAuth(AuthBasicAuthenticator):
214 class BasicAuth(AuthBasicAuthenticator):
215
215
216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 initial_call_detection=False, acl_repo_name=None):
217 initial_call_detection=False, acl_repo_name=None):
218 self.realm = realm
218 self.realm = realm
219 self.initial_call = initial_call_detection
219 self.initial_call = initial_call_detection
220 self.authfunc = authfunc
220 self.authfunc = authfunc
221 self.registry = registry
221 self.registry = registry
222 self.acl_repo_name = acl_repo_name
222 self.acl_repo_name = acl_repo_name
223 self._rc_auth_http_code = auth_http_code
223 self._rc_auth_http_code = auth_http_code
224
224
225 def _get_response_from_code(self, http_code):
225 def _get_response_from_code(self, http_code):
226 try:
226 try:
227 return get_exception(safe_int(http_code))
227 return get_exception(safe_int(http_code))
228 except Exception:
228 except Exception:
229 log.exception('Failed to fetch response for code %s' % http_code)
229 log.exception('Failed to fetch response for code %s' % http_code)
230 return HTTPForbidden
230 return HTTPForbidden
231
231
232 def build_authentication(self):
232 def build_authentication(self):
233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 if self._rc_auth_http_code and not self.initial_call:
234 if self._rc_auth_http_code and not self.initial_call:
235 # return alternative HTTP code if alternative http return code
235 # return alternative HTTP code if alternative http return code
236 # is specified in RhodeCode config, but ONLY if it's not the
236 # is specified in RhodeCode config, but ONLY if it's not the
237 # FIRST call
237 # FIRST call
238 custom_response_klass = self._get_response_from_code(
238 custom_response_klass = self._get_response_from_code(
239 self._rc_auth_http_code)
239 self._rc_auth_http_code)
240 return custom_response_klass(headers=head)
240 return custom_response_klass(headers=head)
241 return HTTPUnauthorized(headers=head)
241 return HTTPUnauthorized(headers=head)
242
242
243 def authenticate(self, environ):
243 def authenticate(self, environ):
244 authorization = AUTHORIZATION(environ)
244 authorization = AUTHORIZATION(environ)
245 if not authorization:
245 if not authorization:
246 return self.build_authentication()
246 return self.build_authentication()
247 (authmeth, auth) = authorization.split(' ', 1)
247 (authmeth, auth) = authorization.split(' ', 1)
248 if 'basic' != authmeth.lower():
248 if 'basic' != authmeth.lower():
249 return self.build_authentication()
249 return self.build_authentication()
250 auth = auth.strip().decode('base64')
250 auth = auth.strip().decode('base64')
251 _parts = auth.split(':', 1)
251 _parts = auth.split(':', 1)
252 if len(_parts) == 2:
252 if len(_parts) == 2:
253 username, password = _parts
253 username, password = _parts
254 if self.authfunc(
254 if self.authfunc(
255 username, password, environ, VCS_TYPE,
255 username, password, environ, VCS_TYPE,
256 registry=self.registry, acl_repo_name=self.acl_repo_name):
256 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 return username
257 return username
258 if username and password:
258 if username and password:
259 # we mark that we actually executed authentication once, at
259 # we mark that we actually executed authentication once, at
260 # that point we can use the alternative auth code
260 # that point we can use the alternative auth code
261 self.initial_call = False
261 self.initial_call = False
262
262
263 return self.build_authentication()
263 return self.build_authentication()
264
264
265 __call__ = authenticate
265 __call__ = authenticate
266
266
267
267
268 def attach_context_attributes(context, request, user_id, attach_to_request=False):
269 def calculate_version_hash():
268 def calculate_version_hash():
270 return md5(
269 return md5(
271 config.get('beaker.session.secret', '') +
270 config.get('beaker.session.secret', '') +
272 rhodecode.__version__)[:8]
271 rhodecode.__version__)[:8]
273
272
274
273
274 def attach_context_attributes(context, request, user_id):
275 """
275 """
276 Attach variables into template context called `c`, please note that
276 Attach variables into template context called `c`, please note that
277 request could be pylons or pyramid request in here.
277 request could be pylons or pyramid request in here.
278 """
278 """
279 rc_config = SettingsModel().get_all_settings(cache=True)
279 rc_config = SettingsModel().get_all_settings(cache=True)
280
280
281 context.rhodecode_version = rhodecode.__version__
281 context.rhodecode_version = rhodecode.__version__
282 context.rhodecode_edition = config.get('rhodecode.edition')
282 context.rhodecode_edition = config.get('rhodecode.edition')
283 # unique secret + version does not leak the version but keep consistency
283 # unique secret + version does not leak the version but keep consistency
284 context.rhodecode_version_hash = calculate_version_hash()
284 context.rhodecode_version_hash = calculate_version_hash()
285
285
286 # Default language set for the incoming request
286 # Default language set for the incoming request
287 context.language = translation.get_lang()[0]
287 context.language = translation.get_lang()[0]
288
288
289 # Visual options
289 # Visual options
290 context.visual = AttributeDict({})
290 context.visual = AttributeDict({})
291
291
292 # DB stored Visual Items
292 # DB stored Visual Items
293 context.visual.show_public_icon = str2bool(
293 context.visual.show_public_icon = str2bool(
294 rc_config.get('rhodecode_show_public_icon'))
294 rc_config.get('rhodecode_show_public_icon'))
295 context.visual.show_private_icon = str2bool(
295 context.visual.show_private_icon = str2bool(
296 rc_config.get('rhodecode_show_private_icon'))
296 rc_config.get('rhodecode_show_private_icon'))
297 context.visual.stylify_metatags = str2bool(
297 context.visual.stylify_metatags = str2bool(
298 rc_config.get('rhodecode_stylify_metatags'))
298 rc_config.get('rhodecode_stylify_metatags'))
299 context.visual.dashboard_items = safe_int(
299 context.visual.dashboard_items = safe_int(
300 rc_config.get('rhodecode_dashboard_items', 100))
300 rc_config.get('rhodecode_dashboard_items', 100))
301 context.visual.admin_grid_items = safe_int(
301 context.visual.admin_grid_items = safe_int(
302 rc_config.get('rhodecode_admin_grid_items', 100))
302 rc_config.get('rhodecode_admin_grid_items', 100))
303 context.visual.repository_fields = str2bool(
303 context.visual.repository_fields = str2bool(
304 rc_config.get('rhodecode_repository_fields'))
304 rc_config.get('rhodecode_repository_fields'))
305 context.visual.show_version = str2bool(
305 context.visual.show_version = str2bool(
306 rc_config.get('rhodecode_show_version'))
306 rc_config.get('rhodecode_show_version'))
307 context.visual.use_gravatar = str2bool(
307 context.visual.use_gravatar = str2bool(
308 rc_config.get('rhodecode_use_gravatar'))
308 rc_config.get('rhodecode_use_gravatar'))
309 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
309 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
310 context.visual.default_renderer = rc_config.get(
310 context.visual.default_renderer = rc_config.get(
311 'rhodecode_markup_renderer', 'rst')
311 'rhodecode_markup_renderer', 'rst')
312 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
312 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
313 context.visual.rhodecode_support_url = \
313 context.visual.rhodecode_support_url = \
314 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
314 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
315
315
316 context.pre_code = rc_config.get('rhodecode_pre_code')
316 context.pre_code = rc_config.get('rhodecode_pre_code')
317 context.post_code = rc_config.get('rhodecode_post_code')
317 context.post_code = rc_config.get('rhodecode_post_code')
318 context.rhodecode_name = rc_config.get('rhodecode_title')
318 context.rhodecode_name = rc_config.get('rhodecode_title')
319 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
319 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
320 # if we have specified default_encoding in the request, it has more
320 # if we have specified default_encoding in the request, it has more
321 # priority
321 # priority
322 if request.GET.get('default_encoding'):
322 if request.GET.get('default_encoding'):
323 context.default_encodings.insert(0, request.GET.get('default_encoding'))
323 context.default_encodings.insert(0, request.GET.get('default_encoding'))
324 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
324 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
325
325
326 # INI stored
326 # INI stored
327 context.labs_active = str2bool(
327 context.labs_active = str2bool(
328 config.get('labs_settings_active', 'false'))
328 config.get('labs_settings_active', 'false'))
329 context.visual.allow_repo_location_change = str2bool(
329 context.visual.allow_repo_location_change = str2bool(
330 config.get('allow_repo_location_change', True))
330 config.get('allow_repo_location_change', True))
331 context.visual.allow_custom_hooks_settings = str2bool(
331 context.visual.allow_custom_hooks_settings = str2bool(
332 config.get('allow_custom_hooks_settings', True))
332 config.get('allow_custom_hooks_settings', True))
333 context.debug_style = str2bool(config.get('debug_style', False))
333 context.debug_style = str2bool(config.get('debug_style', False))
334
334
335 context.rhodecode_instanceid = config.get('instance_id')
335 context.rhodecode_instanceid = config.get('instance_id')
336
336
337 context.visual.cut_off_limit_diff = safe_int(
337 context.visual.cut_off_limit_diff = safe_int(
338 config.get('cut_off_limit_diff'))
338 config.get('cut_off_limit_diff'))
339 context.visual.cut_off_limit_file = safe_int(
339 context.visual.cut_off_limit_file = safe_int(
340 config.get('cut_off_limit_file'))
340 config.get('cut_off_limit_file'))
341
341
342 # AppEnlight
342 # AppEnlight
343 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
343 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
344 context.appenlight_api_public_key = config.get(
344 context.appenlight_api_public_key = config.get(
345 'appenlight.api_public_key', '')
345 'appenlight.api_public_key', '')
346 context.appenlight_server_url = config.get('appenlight.server_url', '')
346 context.appenlight_server_url = config.get('appenlight.server_url', '')
347
347
348 # JS template context
348 # JS template context
349 context.template_context = {
349 context.template_context = {
350 'repo_name': None,
350 'repo_name': None,
351 'repo_type': None,
351 'repo_type': None,
352 'repo_landing_commit': None,
352 'repo_landing_commit': None,
353 'rhodecode_user': {
353 'rhodecode_user': {
354 'username': None,
354 'username': None,
355 'email': None,
355 'email': None,
356 'notification_status': False
356 'notification_status': False
357 },
357 },
358 'visual': {
358 'visual': {
359 'default_renderer': None
359 'default_renderer': None
360 },
360 },
361 'commit_data': {
361 'commit_data': {
362 'commit_id': None
362 'commit_id': None
363 },
363 },
364 'pull_request_data': {'pull_request_id': None},
364 'pull_request_data': {'pull_request_id': None},
365 'timeago': {
365 'timeago': {
366 'refresh_time': 120 * 1000,
366 'refresh_time': 120 * 1000,
367 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
367 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
368 },
368 },
369 'pylons_dispatch': {
369 'pylons_dispatch': {
370 # 'controller': request.environ['pylons.routes_dict']['controller'],
370 # 'controller': request.environ['pylons.routes_dict']['controller'],
371 # 'action': request.environ['pylons.routes_dict']['action'],
371 # 'action': request.environ['pylons.routes_dict']['action'],
372 },
372 },
373 'pyramid_dispatch': {
373 'pyramid_dispatch': {
374
374
375 },
375 },
376 'extra': {'plugins': {}}
376 'extra': {'plugins': {}}
377 }
377 }
378 # END CONFIG VARS
378 # END CONFIG VARS
379
379
380 # TODO: This dosn't work when called from pylons compatibility tween.
380 # TODO: This dosn't work when called from pylons compatibility tween.
381 # Fix this and remove it from base controller.
381 # Fix this and remove it from base controller.
382 # context.repo_name = get_repo_slug(request) # can be empty
382 # context.repo_name = get_repo_slug(request) # can be empty
383
383
384 diffmode = 'sideside'
384 diffmode = 'sideside'
385 if request.GET.get('diffmode'):
385 if request.GET.get('diffmode'):
386 if request.GET['diffmode'] == 'unified':
386 if request.GET['diffmode'] == 'unified':
387 diffmode = 'unified'
387 diffmode = 'unified'
388 elif request.session.get('diffmode'):
388 elif request.session.get('diffmode'):
389 diffmode = request.session['diffmode']
389 diffmode = request.session['diffmode']
390
390
391 context.diffmode = diffmode
391 context.diffmode = diffmode
392
392
393 if request.session.get('diffmode') != diffmode:
393 if request.session.get('diffmode') != diffmode:
394 request.session['diffmode'] = diffmode
394 request.session['diffmode'] = diffmode
395
395
396 context.csrf_token = auth.get_csrf_token()
396 context.csrf_token = auth.get_csrf_token()
397 context.backends = rhodecode.BACKENDS.keys()
397 context.backends = rhodecode.BACKENDS.keys()
398 context.backends.sort()
398 context.backends.sort()
399 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
399 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
400 if attach_to_request:
400 context.pyramid_request = pyramid.threadlocal.get_current_request()
401 request.call_context = context
402 else:
403 context.pyramid_request = pyramid.threadlocal.get_current_request()
404
401
402 # attach the whole call context to the request
403 request.call_context = context
405
404
406
405
407 def get_auth_user(environ):
406 def get_auth_user(environ):
408 ip_addr = get_ip_addr(environ)
407 ip_addr = get_ip_addr(environ)
409 # make sure that we update permissions each time we call controller
408 # make sure that we update permissions each time we call controller
410 _auth_token = (request.GET.get('auth_token', '') or
409 _auth_token = (request.GET.get('auth_token', '') or
411 request.GET.get('api_key', ''))
410 request.GET.get('api_key', ''))
412
411
413 if _auth_token:
412 if _auth_token:
414 # when using API_KEY we assume user exists, and
413 # when using API_KEY we assume user exists, and
415 # doesn't need auth based on cookies.
414 # doesn't need auth based on cookies.
416 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
415 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
417 authenticated = False
416 authenticated = False
418 else:
417 else:
419 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
418 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
420 try:
419 try:
421 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
420 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
422 ip_addr=ip_addr)
421 ip_addr=ip_addr)
423 except UserCreationError as e:
422 except UserCreationError as e:
424 h.flash(e, 'error')
423 h.flash(e, 'error')
425 # container auth or other auth functions that create users
424 # container auth or other auth functions that create users
426 # on the fly can throw this exception signaling that there's
425 # on the fly can throw this exception signaling that there's
427 # issue with user creation, explanation should be provided
426 # issue with user creation, explanation should be provided
428 # in Exception itself. We then create a simple blank
427 # in Exception itself. We then create a simple blank
429 # AuthUser
428 # AuthUser
430 auth_user = AuthUser(ip_addr=ip_addr)
429 auth_user = AuthUser(ip_addr=ip_addr)
431
430
432 if password_changed(auth_user, session):
431 if password_changed(auth_user, session):
433 session.invalidate()
432 session.invalidate()
434 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
433 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
435 auth_user = AuthUser(ip_addr=ip_addr)
434 auth_user = AuthUser(ip_addr=ip_addr)
436
435
437 authenticated = cookie_store.get('is_authenticated')
436 authenticated = cookie_store.get('is_authenticated')
438
437
439 if not auth_user.is_authenticated and auth_user.is_user_object:
438 if not auth_user.is_authenticated and auth_user.is_user_object:
440 # user is not authenticated and not empty
439 # user is not authenticated and not empty
441 auth_user.set_authenticated(authenticated)
440 auth_user.set_authenticated(authenticated)
442
441
443 return auth_user
442 return auth_user
444
443
445
444
446 class BaseController(WSGIController):
445 class BaseController(WSGIController):
447
446
448 def __before__(self):
447 def __before__(self):
449 """
448 """
450 __before__ is called before controller methods and after __call__
449 __before__ is called before controller methods and after __call__
451 """
450 """
452 # on each call propagate settings calls into global settings.
451 # on each call propagate settings calls into global settings.
453 set_rhodecode_config(config)
452 set_rhodecode_config(config)
454 attach_context_attributes(c, request, c.rhodecode_user.user_id)
453 attach_context_attributes(c, request, c.rhodecode_user.user_id)
455
454
456 # TODO: Remove this when fixed in attach_context_attributes()
455 # TODO: Remove this when fixed in attach_context_attributes()
457 c.repo_name = get_repo_slug(request) # can be empty
456 c.repo_name = get_repo_slug(request) # can be empty
458
457
459 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
458 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
460 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
459 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
461 self.sa = meta.Session
460 self.sa = meta.Session
462 self.scm_model = ScmModel(self.sa)
461 self.scm_model = ScmModel(self.sa)
463
462
464 # set user language
463 # set user language
465 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
464 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
466 if user_lang:
465 if user_lang:
467 translation.set_lang(user_lang)
466 translation.set_lang(user_lang)
468 log.debug('set language to %s for user %s',
467 log.debug('set language to %s for user %s',
469 user_lang, self._rhodecode_user)
468 user_lang, self._rhodecode_user)
470
469
471 def _dispatch_redirect(self, with_url, environ, start_response):
470 def _dispatch_redirect(self, with_url, environ, start_response):
472 resp = HTTPFound(with_url)
471 resp = HTTPFound(with_url)
473 environ['SCRIPT_NAME'] = '' # handle prefix middleware
472 environ['SCRIPT_NAME'] = '' # handle prefix middleware
474 environ['PATH_INFO'] = with_url
473 environ['PATH_INFO'] = with_url
475 return resp(environ, start_response)
474 return resp(environ, start_response)
476
475
477 def __call__(self, environ, start_response):
476 def __call__(self, environ, start_response):
478 """Invoke the Controller"""
477 """Invoke the Controller"""
479 # WSGIController.__call__ dispatches to the Controller method
478 # WSGIController.__call__ dispatches to the Controller method
480 # the request is routed to. This routing information is
479 # the request is routed to. This routing information is
481 # available in environ['pylons.routes_dict']
480 # available in environ['pylons.routes_dict']
482 from rhodecode.lib import helpers as h
481 from rhodecode.lib import helpers as h
483
482
484 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
483 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
485 if environ.get('debugtoolbar.wants_pylons_context', False):
484 if environ.get('debugtoolbar.wants_pylons_context', False):
486 environ['debugtoolbar.pylons_context'] = c._current_obj()
485 environ['debugtoolbar.pylons_context'] = c._current_obj()
487
486
488 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
487 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
489 environ['pylons.routes_dict']['action']])
488 environ['pylons.routes_dict']['action']])
490
489
491 self.rc_config = SettingsModel().get_all_settings(cache=True)
490 self.rc_config = SettingsModel().get_all_settings(cache=True)
492 self.ip_addr = get_ip_addr(environ)
491 self.ip_addr = get_ip_addr(environ)
493
492
494 # The rhodecode auth user is looked up and passed through the
493 # The rhodecode auth user is looked up and passed through the
495 # environ by the pylons compatibility tween in pyramid.
494 # environ by the pylons compatibility tween in pyramid.
496 # So we can just grab it from there.
495 # So we can just grab it from there.
497 auth_user = environ['rc_auth_user']
496 auth_user = environ['rc_auth_user']
498
497
499 # set globals for auth user
498 # set globals for auth user
500 request.user = auth_user
499 request.user = auth_user
501 c.rhodecode_user = self._rhodecode_user = auth_user
500 c.rhodecode_user = self._rhodecode_user = auth_user
502
501
503 log.info('IP: %s User: %s accessed %s [%s]' % (
502 log.info('IP: %s User: %s accessed %s [%s]' % (
504 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
503 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
505 _route_name)
504 _route_name)
506 )
505 )
507
506
508 user_obj = auth_user.get_instance()
507 user_obj = auth_user.get_instance()
509 if user_obj and user_obj.user_data.get('force_password_change'):
508 if user_obj and user_obj.user_data.get('force_password_change'):
510 h.flash('You are required to change your password', 'warning',
509 h.flash('You are required to change your password', 'warning',
511 ignore_duplicate=True)
510 ignore_duplicate=True)
512 return self._dispatch_redirect(
511 return self._dispatch_redirect(
513 url('my_account_password'), environ, start_response)
512 url('my_account_password'), environ, start_response)
514
513
515 return WSGIController.__call__(self, environ, start_response)
514 return WSGIController.__call__(self, environ, start_response)
516
515
517
516
518 class BaseRepoController(BaseController):
517 class BaseRepoController(BaseController):
519 """
518 """
520 Base class for controllers responsible for loading all needed data for
519 Base class for controllers responsible for loading all needed data for
521 repository loaded items are
520 repository loaded items are
522
521
523 c.rhodecode_repo: instance of scm repository
522 c.rhodecode_repo: instance of scm repository
524 c.rhodecode_db_repo: instance of db
523 c.rhodecode_db_repo: instance of db
525 c.repository_requirements_missing: shows that repository specific data
524 c.repository_requirements_missing: shows that repository specific data
526 could not be displayed due to the missing requirements
525 could not be displayed due to the missing requirements
527 c.repository_pull_requests: show number of open pull requests
526 c.repository_pull_requests: show number of open pull requests
528 """
527 """
529
528
530 def __before__(self):
529 def __before__(self):
531 super(BaseRepoController, self).__before__()
530 super(BaseRepoController, self).__before__()
532 if c.repo_name: # extracted from routes
531 if c.repo_name: # extracted from routes
533 db_repo = Repository.get_by_repo_name(c.repo_name)
532 db_repo = Repository.get_by_repo_name(c.repo_name)
534 if not db_repo:
533 if not db_repo:
535 return
534 return
536
535
537 log.debug(
536 log.debug(
538 'Found repository in database %s with state `%s`',
537 'Found repository in database %s with state `%s`',
539 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
538 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
540 route = getattr(request.environ.get('routes.route'), 'name', '')
539 route = getattr(request.environ.get('routes.route'), 'name', '')
541
540
542 # allow to delete repos that are somehow damages in filesystem
541 # allow to delete repos that are somehow damages in filesystem
543 if route in ['delete_repo']:
542 if route in ['delete_repo']:
544 return
543 return
545
544
546 if db_repo.repo_state in [Repository.STATE_PENDING]:
545 if db_repo.repo_state in [Repository.STATE_PENDING]:
547 if route in ['repo_creating_home']:
546 if route in ['repo_creating_home']:
548 return
547 return
549 check_url = url('repo_creating_home', repo_name=c.repo_name)
548 check_url = url('repo_creating_home', repo_name=c.repo_name)
550 return redirect(check_url)
549 return redirect(check_url)
551
550
552 self.rhodecode_db_repo = db_repo
551 self.rhodecode_db_repo = db_repo
553
552
554 missing_requirements = False
553 missing_requirements = False
555 try:
554 try:
556 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
555 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
557 except RepositoryRequirementError as e:
556 except RepositoryRequirementError as e:
558 missing_requirements = True
557 missing_requirements = True
559 self._handle_missing_requirements(e)
558 self._handle_missing_requirements(e)
560
559
561 if self.rhodecode_repo is None and not missing_requirements:
560 if self.rhodecode_repo is None and not missing_requirements:
562 log.error('%s this repository is present in database but it '
561 log.error('%s this repository is present in database but it '
563 'cannot be created as an scm instance', c.repo_name)
562 'cannot be created as an scm instance', c.repo_name)
564
563
565 h.flash(_(
564 h.flash(_(
566 "The repository at %(repo_name)s cannot be located.") %
565 "The repository at %(repo_name)s cannot be located.") %
567 {'repo_name': c.repo_name},
566 {'repo_name': c.repo_name},
568 category='error', ignore_duplicate=True)
567 category='error', ignore_duplicate=True)
569 redirect(h.route_path('home'))
568 redirect(h.route_path('home'))
570
569
571 # update last change according to VCS data
570 # update last change according to VCS data
572 if not missing_requirements:
571 if not missing_requirements:
573 commit = db_repo.get_commit(
572 commit = db_repo.get_commit(
574 pre_load=["author", "date", "message", "parents"])
573 pre_load=["author", "date", "message", "parents"])
575 db_repo.update_commit_cache(commit)
574 db_repo.update_commit_cache(commit)
576
575
577 # Prepare context
576 # Prepare context
578 c.rhodecode_db_repo = db_repo
577 c.rhodecode_db_repo = db_repo
579 c.rhodecode_repo = self.rhodecode_repo
578 c.rhodecode_repo = self.rhodecode_repo
580 c.repository_requirements_missing = missing_requirements
579 c.repository_requirements_missing = missing_requirements
581
580
582 self._update_global_counters(self.scm_model, db_repo)
581 self._update_global_counters(self.scm_model, db_repo)
583
582
584 def _update_global_counters(self, scm_model, db_repo):
583 def _update_global_counters(self, scm_model, db_repo):
585 """
584 """
586 Base variables that are exposed to every page of repository
585 Base variables that are exposed to every page of repository
587 """
586 """
588 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
587 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
589
588
590 def _handle_missing_requirements(self, error):
589 def _handle_missing_requirements(self, error):
591 self.rhodecode_repo = None
590 self.rhodecode_repo = None
592 log.error(
591 log.error(
593 'Requirements are missing for repository %s: %s',
592 'Requirements are missing for repository %s: %s',
594 c.repo_name, error.message)
593 c.repo_name, error.message)
595
594
596 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
595 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
597 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
596 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
598 settings_update_url = url('repo', repo_name=c.repo_name)
597 settings_update_url = url('repo', repo_name=c.repo_name)
599 path = request.path
598 path = request.path
600 should_redirect = (
599 should_redirect = (
601 path not in (summary_url, settings_update_url)
600 path not in (summary_url, settings_update_url)
602 and '/settings' not in path or path == statistics_url
601 and '/settings' not in path or path == statistics_url
603 )
602 )
604 if should_redirect:
603 if should_redirect:
605 redirect(summary_url)
604 redirect(summary_url)
General Comments 0
You need to be logged in to leave comments. Login now