##// END OF EJS Templates
security: update lastactivity when on audit logs....
marcink -
r2930:a5198975 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,542 +1,543 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-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 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 request.environ['rc_auth_user_id'] = auth_u.user_id
211
212
212 # now check if token is valid for API
213 # now check if token is valid for API
213 auth_token = request.rpc_api_key
214 auth_token = request.rpc_api_key
214 token_match = api_user.authenticate_by_token(
215 token_match = api_user.authenticate_by_token(
215 auth_token, roles=[UserApiKeys.ROLE_API])
216 auth_token, roles=[UserApiKeys.ROLE_API])
216 invalid_token = not token_match
217 invalid_token = not token_match
217
218
218 log.debug('Checking if API KEY is valid with proper role')
219 log.debug('Checking if API KEY is valid with proper role')
219 if invalid_token:
220 if invalid_token:
220 return jsonrpc_error(
221 return jsonrpc_error(
221 request, retid=request.rpc_id,
222 request, retid=request.rpc_id,
222 message='API KEY invalid or, has bad role for an API call')
223 message='API KEY invalid or, has bad role for an API call')
223
224
224 except Exception:
225 except Exception:
225 log.exception('Error on API AUTH')
226 log.exception('Error on API AUTH')
226 return jsonrpc_error(
227 return jsonrpc_error(
227 request, retid=request.rpc_id, message='Invalid API KEY')
228 request, retid=request.rpc_id, message='Invalid API KEY')
228
229
229 method = request.rpc_method
230 method = request.rpc_method
230 func = request.registry.jsonrpc_methods[method]
231 func = request.registry.jsonrpc_methods[method]
231
232
232 # now that we have a method, add request._req_params to
233 # now that we have a method, add request._req_params to
233 # self.kargs and dispatch control to WGIController
234 # self.kargs and dispatch control to WGIController
234 argspec = inspect.getargspec(func)
235 argspec = inspect.getargspec(func)
235 arglist = argspec[0]
236 arglist = argspec[0]
236 defaults = map(type, argspec[3] or [])
237 defaults = map(type, argspec[3] or [])
237 default_empty = types.NotImplementedType
238 default_empty = types.NotImplementedType
238
239
239 # kw arguments required by this method
240 # kw arguments required by this method
240 func_kwargs = dict(itertools.izip_longest(
241 func_kwargs = dict(itertools.izip_longest(
241 reversed(arglist), reversed(defaults), fillvalue=default_empty))
242 reversed(arglist), reversed(defaults), fillvalue=default_empty))
242
243
243 # This attribute will need to be first param of a method that uses
244 # 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
245 # api_key, which is translated to instance of user at that name
245 user_var = 'apiuser'
246 user_var = 'apiuser'
246 request_var = 'request'
247 request_var = 'request'
247
248
248 for arg in [user_var, request_var]:
249 for arg in [user_var, request_var]:
249 if arg not in arglist:
250 if arg not in arglist:
250 return jsonrpc_error(
251 return jsonrpc_error(
251 request,
252 request,
252 retid=request.rpc_id,
253 retid=request.rpc_id,
253 message='This method [%s] does not support '
254 message='This method [%s] does not support '
254 'required parameter `%s`' % (func.__name__, arg))
255 'required parameter `%s`' % (func.__name__, arg))
255
256
256 # get our arglist and check if we provided them as args
257 # get our arglist and check if we provided them as args
257 for arg, default in func_kwargs.items():
258 for arg, default in func_kwargs.items():
258 if arg in [user_var, request_var]:
259 if arg in [user_var, request_var]:
259 # user_var and request_var are pre-hardcoded parameters and we
260 # user_var and request_var are pre-hardcoded parameters and we
260 # don't need to do any translation
261 # don't need to do any translation
261 continue
262 continue
262
263
263 # skip the required param check if it's default value is
264 # skip the required param check if it's default value is
264 # NotImplementedType (default_empty)
265 # NotImplementedType (default_empty)
265 if default == default_empty and arg not in request.rpc_params:
266 if default == default_empty and arg not in request.rpc_params:
266 return jsonrpc_error(
267 return jsonrpc_error(
267 request,
268 request,
268 retid=request.rpc_id,
269 retid=request.rpc_id,
269 message=('Missing non optional `%s` arg in JSON DATA' % arg)
270 message=('Missing non optional `%s` arg in JSON DATA' % arg)
270 )
271 )
271
272
272 # sanitize extra passed arguments
273 # sanitize extra passed arguments
273 for k in request.rpc_params.keys()[:]:
274 for k in request.rpc_params.keys()[:]:
274 if k not in func_kwargs:
275 if k not in func_kwargs:
275 del request.rpc_params[k]
276 del request.rpc_params[k]
276
277
277 call_params = request.rpc_params
278 call_params = request.rpc_params
278 call_params.update({
279 call_params.update({
279 'request': request,
280 'request': request,
280 'apiuser': auth_u
281 'apiuser': auth_u
281 })
282 })
282
283
283 # register some common functions for usage
284 # register some common functions for usage
284 attach_context_attributes(
285 attach_context_attributes(
285 TemplateArgs(), request, request.rpc_user.user_id)
286 TemplateArgs(), request, request.rpc_user.user_id)
286
287
287 try:
288 try:
288 ret_value = func(**call_params)
289 ret_value = func(**call_params)
289 return jsonrpc_response(request, ret_value)
290 return jsonrpc_response(request, ret_value)
290 except JSONRPCBaseError:
291 except JSONRPCBaseError:
291 raise
292 raise
292 except Exception:
293 except Exception:
293 log.exception('Unhandled exception occurred on api call: %s', func)
294 log.exception('Unhandled exception occurred on api call: %s', func)
294 return jsonrpc_error(request, retid=request.rpc_id,
295 return jsonrpc_error(request, retid=request.rpc_id,
295 message='Internal server error')
296 message='Internal server error')
296
297
297
298
298 def setup_request(request):
299 def setup_request(request):
299 """
300 """
300 Parse a JSON-RPC request body. It's used inside the predicates method
301 Parse a JSON-RPC request body. It's used inside the predicates method
301 to validate and bootstrap requests for usage in rpc calls.
302 to validate and bootstrap requests for usage in rpc calls.
302
303
303 We need to raise JSONRPCError here if we want to return some errors back to
304 We need to raise JSONRPCError here if we want to return some errors back to
304 user.
305 user.
305 """
306 """
306
307
307 log.debug('Executing setup request: %r', request)
308 log.debug('Executing setup request: %r', request)
308 request.rpc_ip_addr = get_ip_addr(request.environ)
309 request.rpc_ip_addr = get_ip_addr(request.environ)
309 # TODO(marcink): deprecate GET at some point
310 # TODO(marcink): deprecate GET at some point
310 if request.method not in ['POST', 'GET']:
311 if request.method not in ['POST', 'GET']:
311 log.debug('unsupported request method "%s"', request.method)
312 log.debug('unsupported request method "%s"', request.method)
312 raise JSONRPCError(
313 raise JSONRPCError(
313 'unsupported request method "%s". Please use POST' % request.method)
314 'unsupported request method "%s". Please use POST' % request.method)
314
315
315 if 'CONTENT_LENGTH' not in request.environ:
316 if 'CONTENT_LENGTH' not in request.environ:
316 log.debug("No Content-Length")
317 log.debug("No Content-Length")
317 raise JSONRPCError("Empty body, No Content-Length in request")
318 raise JSONRPCError("Empty body, No Content-Length in request")
318
319
319 else:
320 else:
320 length = request.environ['CONTENT_LENGTH']
321 length = request.environ['CONTENT_LENGTH']
321 log.debug('Content-Length: %s', length)
322 log.debug('Content-Length: %s', length)
322
323
323 if length == 0:
324 if length == 0:
324 log.debug("Content-Length is 0")
325 log.debug("Content-Length is 0")
325 raise JSONRPCError("Content-Length is 0")
326 raise JSONRPCError("Content-Length is 0")
326
327
327 raw_body = request.body
328 raw_body = request.body
328 try:
329 try:
329 json_body = json.loads(raw_body)
330 json_body = json.loads(raw_body)
330 except ValueError as e:
331 except ValueError as e:
331 # catch JSON errors Here
332 # catch JSON errors Here
332 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
333 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
333
334
334 request.rpc_id = json_body.get('id')
335 request.rpc_id = json_body.get('id')
335 request.rpc_method = json_body.get('method')
336 request.rpc_method = json_body.get('method')
336
337
337 # check required base parameters
338 # check required base parameters
338 try:
339 try:
339 api_key = json_body.get('api_key')
340 api_key = json_body.get('api_key')
340 if not api_key:
341 if not api_key:
341 api_key = json_body.get('auth_token')
342 api_key = json_body.get('auth_token')
342
343
343 if not api_key:
344 if not api_key:
344 raise KeyError('api_key or auth_token')
345 raise KeyError('api_key or auth_token')
345
346
346 # TODO(marcink): support passing in token in request header
347 # TODO(marcink): support passing in token in request header
347
348
348 request.rpc_api_key = api_key
349 request.rpc_api_key = api_key
349 request.rpc_id = json_body['id']
350 request.rpc_id = json_body['id']
350 request.rpc_method = json_body['method']
351 request.rpc_method = json_body['method']
351 request.rpc_params = json_body['args'] \
352 request.rpc_params = json_body['args'] \
352 if isinstance(json_body['args'], dict) else {}
353 if isinstance(json_body['args'], dict) else {}
353
354
354 log.debug(
355 log.debug(
355 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
356 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
356 except KeyError as e:
357 except KeyError as e:
357 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
358 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
358
359
359 log.debug('setup complete, now handling method:%s rpcid:%s',
360 log.debug('setup complete, now handling method:%s rpcid:%s',
360 request.rpc_method, request.rpc_id, )
361 request.rpc_method, request.rpc_id, )
361
362
362
363
363 class RoutePredicate(object):
364 class RoutePredicate(object):
364 def __init__(self, val, config):
365 def __init__(self, val, config):
365 self.val = val
366 self.val = val
366
367
367 def text(self):
368 def text(self):
368 return 'jsonrpc route = %s' % self.val
369 return 'jsonrpc route = %s' % self.val
369
370
370 phash = text
371 phash = text
371
372
372 def __call__(self, info, request):
373 def __call__(self, info, request):
373 if self.val:
374 if self.val:
374 # potentially setup and bootstrap our call
375 # potentially setup and bootstrap our call
375 setup_request(request)
376 setup_request(request)
376
377
377 # Always return True so that even if it isn't a valid RPC it
378 # 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
379 # will fall through to the underlaying handlers like notfound_view
379 return True
380 return True
380
381
381
382
382 class NotFoundPredicate(object):
383 class NotFoundPredicate(object):
383 def __init__(self, val, config):
384 def __init__(self, val, config):
384 self.val = val
385 self.val = val
385 self.methods = config.registry.jsonrpc_methods
386 self.methods = config.registry.jsonrpc_methods
386
387
387 def text(self):
388 def text(self):
388 return 'jsonrpc method not found = {}.'.format(self.val)
389 return 'jsonrpc method not found = {}.'.format(self.val)
389
390
390 phash = text
391 phash = text
391
392
392 def __call__(self, info, request):
393 def __call__(self, info, request):
393 return hasattr(request, 'rpc_method')
394 return hasattr(request, 'rpc_method')
394
395
395
396
396 class MethodPredicate(object):
397 class MethodPredicate(object):
397 def __init__(self, val, config):
398 def __init__(self, val, config):
398 self.method = val
399 self.method = val
399
400
400 def text(self):
401 def text(self):
401 return 'jsonrpc method = %s' % self.method
402 return 'jsonrpc method = %s' % self.method
402
403
403 phash = text
404 phash = text
404
405
405 def __call__(self, context, request):
406 def __call__(self, context, request):
406 # we need to explicitly return False here, so pyramid doesn't try to
407 # 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
408 # execute our view directly. We need our main handler to execute things
408 return getattr(request, 'rpc_method') == self.method
409 return getattr(request, 'rpc_method') == self.method
409
410
410
411
411 def add_jsonrpc_method(config, view, **kwargs):
412 def add_jsonrpc_method(config, view, **kwargs):
412 # pop the method name
413 # pop the method name
413 method = kwargs.pop('method', None)
414 method = kwargs.pop('method', None)
414
415
415 if method is None:
416 if method is None:
416 raise ConfigurationError(
417 raise ConfigurationError(
417 'Cannot register a JSON-RPC method without specifying the '
418 'Cannot register a JSON-RPC method without specifying the '
418 '"method"')
419 '"method"')
419
420
420 # we define custom predicate, to enable to detect conflicting methods,
421 # we define custom predicate, to enable to detect conflicting methods,
421 # those predicates are kind of "translation" from the decorator variables
422 # those predicates are kind of "translation" from the decorator variables
422 # to internal predicates names
423 # to internal predicates names
423
424
424 kwargs['jsonrpc_method'] = method
425 kwargs['jsonrpc_method'] = method
425
426
426 # register our view into global view store for validation
427 # register our view into global view store for validation
427 config.registry.jsonrpc_methods[method] = view
428 config.registry.jsonrpc_methods[method] = view
428
429
429 # we're using our main request_view handler, here, so each method
430 # we're using our main request_view handler, here, so each method
430 # has a unified handler for itself
431 # has a unified handler for itself
431 config.add_view(request_view, route_name='apiv2', **kwargs)
432 config.add_view(request_view, route_name='apiv2', **kwargs)
432
433
433
434
434 class jsonrpc_method(object):
435 class jsonrpc_method(object):
435 """
436 """
436 decorator that works similar to @add_view_config decorator,
437 decorator that works similar to @add_view_config decorator,
437 but tailored for our JSON RPC
438 but tailored for our JSON RPC
438 """
439 """
439
440
440 venusian = venusian # for testing injection
441 venusian = venusian # for testing injection
441
442
442 def __init__(self, method=None, **kwargs):
443 def __init__(self, method=None, **kwargs):
443 self.method = method
444 self.method = method
444 self.kwargs = kwargs
445 self.kwargs = kwargs
445
446
446 def __call__(self, wrapped):
447 def __call__(self, wrapped):
447 kwargs = self.kwargs.copy()
448 kwargs = self.kwargs.copy()
448 kwargs['method'] = self.method or wrapped.__name__
449 kwargs['method'] = self.method or wrapped.__name__
449 depth = kwargs.pop('_depth', 0)
450 depth = kwargs.pop('_depth', 0)
450
451
451 def callback(context, name, ob):
452 def callback(context, name, ob):
452 config = context.config.with_package(info.module)
453 config = context.config.with_package(info.module)
453 config.add_jsonrpc_method(view=ob, **kwargs)
454 config.add_jsonrpc_method(view=ob, **kwargs)
454
455
455 info = venusian.attach(wrapped, callback, category='pyramid',
456 info = venusian.attach(wrapped, callback, category='pyramid',
456 depth=depth + 1)
457 depth=depth + 1)
457 if info.scope == 'class':
458 if info.scope == 'class':
458 # ensure that attr is set if decorating a class method
459 # ensure that attr is set if decorating a class method
459 kwargs.setdefault('attr', wrapped.__name__)
460 kwargs.setdefault('attr', wrapped.__name__)
460
461
461 kwargs['_info'] = info.codeinfo # fbo action_method
462 kwargs['_info'] = info.codeinfo # fbo action_method
462 return wrapped
463 return wrapped
463
464
464
465
465 class jsonrpc_deprecated_method(object):
466 class jsonrpc_deprecated_method(object):
466 """
467 """
467 Marks method as deprecated, adds log.warning, and inject special key to
468 Marks method as deprecated, adds log.warning, and inject special key to
468 the request variable to mark method as deprecated.
469 the request variable to mark method as deprecated.
469 Also injects special docstring that extract_docs will catch to mark
470 Also injects special docstring that extract_docs will catch to mark
470 method as deprecated.
471 method as deprecated.
471
472
472 :param use_method: specify which method should be used instead of
473 :param use_method: specify which method should be used instead of
473 the decorated one
474 the decorated one
474
475
475 Use like::
476 Use like::
476
477
477 @jsonrpc_method()
478 @jsonrpc_method()
478 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
479 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
479 def old_func(request, apiuser, arg1, arg2):
480 def old_func(request, apiuser, arg1, arg2):
480 ...
481 ...
481 """
482 """
482
483
483 def __init__(self, use_method, deprecated_at_version):
484 def __init__(self, use_method, deprecated_at_version):
484 self.use_method = use_method
485 self.use_method = use_method
485 self.deprecated_at_version = deprecated_at_version
486 self.deprecated_at_version = deprecated_at_version
486 self.deprecated_msg = ''
487 self.deprecated_msg = ''
487
488
488 def __call__(self, func):
489 def __call__(self, func):
489 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
490 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
490 method=self.use_method)
491 method=self.use_method)
491
492
492 docstring = """\n
493 docstring = """\n
493 .. deprecated:: {version}
494 .. deprecated:: {version}
494
495
495 {deprecation_message}
496 {deprecation_message}
496
497
497 {original_docstring}
498 {original_docstring}
498 """
499 """
499 func.__doc__ = docstring.format(
500 func.__doc__ = docstring.format(
500 version=self.deprecated_at_version,
501 version=self.deprecated_at_version,
501 deprecation_message=self.deprecated_msg,
502 deprecation_message=self.deprecated_msg,
502 original_docstring=func.__doc__)
503 original_docstring=func.__doc__)
503 return decorator.decorator(self.__wrapper, func)
504 return decorator.decorator(self.__wrapper, func)
504
505
505 def __wrapper(self, func, *fargs, **fkwargs):
506 def __wrapper(self, func, *fargs, **fkwargs):
506 log.warning('DEPRECATED API CALL on function %s, please '
507 log.warning('DEPRECATED API CALL on function %s, please '
507 'use `%s` instead', func, self.use_method)
508 'use `%s` instead', func, self.use_method)
508 # alter function docstring to mark as deprecated, this is picked up
509 # alter function docstring to mark as deprecated, this is picked up
509 # via fabric file that generates API DOC.
510 # via fabric file that generates API DOC.
510 result = func(*fargs, **fkwargs)
511 result = func(*fargs, **fkwargs)
511
512
512 request = fargs[0]
513 request = fargs[0]
513 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
514 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
514 return result
515 return result
515
516
516
517
517 def includeme(config):
518 def includeme(config):
518 plugin_module = 'rhodecode.api'
519 plugin_module = 'rhodecode.api'
519 plugin_settings = get_plugin_settings(
520 plugin_settings = get_plugin_settings(
520 plugin_module, config.registry.settings)
521 plugin_module, config.registry.settings)
521
522
522 if not hasattr(config.registry, 'jsonrpc_methods'):
523 if not hasattr(config.registry, 'jsonrpc_methods'):
523 config.registry.jsonrpc_methods = OrderedDict()
524 config.registry.jsonrpc_methods = OrderedDict()
524
525
525 # match filter by given method only
526 # match filter by given method only
526 config.add_view_predicate('jsonrpc_method', MethodPredicate)
527 config.add_view_predicate('jsonrpc_method', MethodPredicate)
527
528
528 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
529 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
529 serializer=json.dumps, indent=4))
530 serializer=json.dumps, indent=4))
530 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
531 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
531
532
532 config.add_route_predicate(
533 config.add_route_predicate(
533 'jsonrpc_call', RoutePredicate)
534 'jsonrpc_call', RoutePredicate)
534
535
535 config.add_route(
536 config.add_route(
536 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
537 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
537
538
538 config.scan(plugin_module, ignore='rhodecode.api.tests')
539 config.scan(plugin_module, ignore='rhodecode.api.tests')
539 # register some exception handling view
540 # register some exception handling view
540 config.add_view(exception_view, context=JSONRPCBaseError)
541 config.add_view(exception_view, context=JSONRPCBaseError)
541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
542 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
542 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
543 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,116 +1,120 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 mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.db import User
24 from rhodecode.model.db import User
25 from rhodecode.model.user import UserModel
25 from rhodecode.model.user import UserModel
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_ok, assert_error, crash, jsonify)
28 build_data, api_call, assert_ok, assert_error, crash, jsonify)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestUpdateUser(object):
32 class TestUpdateUser(object):
33 @pytest.mark.parametrize("name, expected", [
33 @pytest.mark.parametrize("name, expected", [
34 ('firstname', 'new_username'),
34 ('firstname', 'new_username'),
35 ('lastname', 'new_username'),
35 ('lastname', 'new_username'),
36 ('email', 'new_username'),
36 ('email', 'new_username'),
37 ('admin', True),
37 ('admin', True),
38 ('admin', False),
38 ('admin', False),
39 ('extern_type', 'ldap'),
39 ('extern_type', 'ldap'),
40 ('extern_type', None),
40 ('extern_type', None),
41 ('extern_name', 'test'),
41 ('extern_name', 'test'),
42 ('extern_name', None),
42 ('extern_name', None),
43 ('active', False),
43 ('active', False),
44 ('active', True),
44 ('active', True),
45 ('password', 'newpass')
45 ('password', 'newpass')
46 ])
46 ])
47 def test_api_update_user(self, name, expected, user_util):
47 def test_api_update_user(self, name, expected, user_util):
48 usr = user_util.create_user()
48 usr = user_util.create_user()
49
49
50 kw = {name: expected, 'userid': usr.user_id}
50 kw = {name: expected, 'userid': usr.user_id}
51 id_, params = build_data(self.apikey, 'update_user', **kw)
51 id_, params = build_data(self.apikey, 'update_user', **kw)
52 response = api_call(self.app, params)
52 response = api_call(self.app, params)
53
53
54 ret = {
54 ret = {
55 'msg': 'updated user ID:%s %s' % (usr.user_id, usr.username),
55 'msg': 'updated user ID:%s %s' % (usr.user_id, usr.username),
56 'user': jsonify(
56 'user': jsonify(
57 UserModel()
57 UserModel()
58 .get_by_username(usr.username)
58 .get_by_username(usr.username)
59 .get_api_data(include_secrets=True)
59 .get_api_data(include_secrets=True)
60 )
60 )
61 }
61 }
62
62
63 expected = ret
63 expected = ret
64 assert_ok(id_, expected, given=response.body)
64 assert_ok(id_, expected, given=response.body)
65
65
66 def test_api_update_user_no_changed_params(self):
66 def test_api_update_user_no_changed_params(self):
67 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
67 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
68 ret = jsonify(usr.get_api_data(include_secrets=True))
68 ret = jsonify(usr.get_api_data(include_secrets=True))
69 id_, params = build_data(
69 id_, params = build_data(
70 self.apikey, 'update_user', userid=TEST_USER_ADMIN_LOGIN)
70 self.apikey, 'update_user', userid=TEST_USER_ADMIN_LOGIN)
71
71
72 response = api_call(self.app, params)
72 response = api_call(self.app, params)
73 ret = {
73 ret = {
74 'msg': 'updated user ID:%s %s' % (
74 'msg': 'updated user ID:%s %s' % (
75 usr.user_id, TEST_USER_ADMIN_LOGIN),
75 usr.user_id, TEST_USER_ADMIN_LOGIN),
76 'user': ret
76 'user': ret
77 }
77 }
78 expected = ret
78 expected = ret
79 expected['user']['last_activity'] = response.json['result']['user'][
80 'last_activity']
79 assert_ok(id_, expected, given=response.body)
81 assert_ok(id_, expected, given=response.body)
80
82
81 def test_api_update_user_by_user_id(self):
83 def test_api_update_user_by_user_id(self):
82 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
84 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
83 ret = jsonify(usr.get_api_data(include_secrets=True))
85 ret = jsonify(usr.get_api_data(include_secrets=True))
84 id_, params = build_data(
86 id_, params = build_data(
85 self.apikey, 'update_user', userid=usr.user_id)
87 self.apikey, 'update_user', userid=usr.user_id)
86
88
87 response = api_call(self.app, params)
89 response = api_call(self.app, params)
88 ret = {
90 ret = {
89 'msg': 'updated user ID:%s %s' % (
91 'msg': 'updated user ID:%s %s' % (
90 usr.user_id, TEST_USER_ADMIN_LOGIN),
92 usr.user_id, TEST_USER_ADMIN_LOGIN),
91 'user': ret
93 'user': ret
92 }
94 }
93 expected = ret
95 expected = ret
96 expected['user']['last_activity'] = response.json['result']['user'][
97 'last_activity']
94 assert_ok(id_, expected, given=response.body)
98 assert_ok(id_, expected, given=response.body)
95
99
96 def test_api_update_user_default_user(self):
100 def test_api_update_user_default_user(self):
97 usr = User.get_default_user()
101 usr = User.get_default_user()
98 id_, params = build_data(
102 id_, params = build_data(
99 self.apikey, 'update_user', userid=usr.user_id)
103 self.apikey, 'update_user', userid=usr.user_id)
100
104
101 response = api_call(self.app, params)
105 response = api_call(self.app, params)
102 expected = 'editing default user is forbidden'
106 expected = 'editing default user is forbidden'
103 assert_error(id_, expected, given=response.body)
107 assert_error(id_, expected, given=response.body)
104
108
105 @mock.patch.object(UserModel, 'update_user', crash)
109 @mock.patch.object(UserModel, 'update_user', crash)
106 def test_api_update_user_when_exception_happens(self):
110 def test_api_update_user_when_exception_happens(self):
107 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
111 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
108 ret = jsonify(usr.get_api_data(include_secrets=True))
112 ret = jsonify(usr.get_api_data(include_secrets=True))
109 id_, params = build_data(
113 id_, params = build_data(
110 self.apikey, 'update_user', userid=usr.user_id)
114 self.apikey, 'update_user', userid=usr.user_id)
111
115
112 response = api_call(self.app, params)
116 response = api_call(self.app, params)
113 ret = 'failed to update user `%s`' % (usr.user_id,)
117 ret = 'failed to update user `%s`' % (usr.user_id,)
114
118
115 expected = ret
119 expected = ret
116 assert_error(id_, expected, given=response.body)
120 assert_error(id_, expected, given=response.body)
@@ -1,518 +1,522 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 sys
22 import sys
23 import logging
23 import logging
24 import collections
24 import collections
25 import tempfile
25 import tempfile
26
26
27 from paste.gzipper import make_gzip_middleware
27 from paste.gzipper import make_gzip_middleware
28 import pyramid.events
28 from pyramid.wsgi import wsgiapp
29 from pyramid.wsgi import wsgiapp
29 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.config import Configurator
31 from pyramid.config import Configurator
31 from pyramid.settings import asbool, aslist
32 from pyramid.settings import asbool, aslist
32 from pyramid.httpexceptions import (
33 from pyramid.httpexceptions import (
33 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
34 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
34 from pyramid.events import ApplicationCreated
35 from pyramid.renderers import render_to_response
35 from pyramid.renderers import render_to_response
36
36
37 from rhodecode.model import meta
37 from rhodecode.model import meta
38 from rhodecode.config import patches
38 from rhodecode.config import patches
39 from rhodecode.config import utils as config_utils
39 from rhodecode.config import utils as config_utils
40 from rhodecode.config.environment import load_pyramid_environment
40 from rhodecode.config.environment import load_pyramid_environment
41
41
42 import rhodecode.events
42 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 from rhodecode.lib.request import Request
44 from rhodecode.lib.request import Request
44 from rhodecode.lib.vcs import VCSCommunicationError
45 from rhodecode.lib.vcs import VCSCommunicationError
45 from rhodecode.lib.exceptions import VCSServerUnavailable
46 from rhodecode.lib.exceptions import VCSServerUnavailable
46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.celerylib.loader import configure_celery
49 from rhodecode.lib.celerylib.loader import configure_celery
49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
51 from rhodecode.lib.exc_tracking import store_exception
52 from rhodecode.lib.exc_tracking import store_exception
52 from rhodecode.subscribers import (
53 from rhodecode.subscribers import (
53 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 write_metadata_if_needed, inject_app_settings)
55 write_metadata_if_needed, inject_app_settings)
55
56
56
57
57 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
58
59
59
60
60 def is_http_error(response):
61 def is_http_error(response):
61 # error which should have traceback
62 # error which should have traceback
62 return response.status_code > 499
63 return response.status_code > 499
63
64
64
65
65 def make_pyramid_app(global_config, **settings):
66 def make_pyramid_app(global_config, **settings):
66 """
67 """
67 Constructs the WSGI application based on Pyramid.
68 Constructs the WSGI application based on Pyramid.
68
69
69 Specials:
70 Specials:
70
71
71 * The application can also be integrated like a plugin via the call to
72 * The application can also be integrated like a plugin via the call to
72 `includeme`. This is accompanied with the other utility functions which
73 `includeme`. This is accompanied with the other utility functions which
73 are called. Changing this should be done with great care to not break
74 are called. Changing this should be done with great care to not break
74 cases when these fragments are assembled from another place.
75 cases when these fragments are assembled from another place.
75
76
76 """
77 """
77
78
78 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
79 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
79 # will be replaced by the value of the environment variable "NAME" in this case.
80 # will be replaced by the value of the environment variable "NAME" in this case.
80 environ = {
81 environ = {
81 'ENV_{}'.format(key): value for key, value in os.environ.items()}
82 'ENV_{}'.format(key): value for key, value in os.environ.items()}
82
83
83 global_config = _substitute_values(global_config, environ)
84 global_config = _substitute_values(global_config, environ)
84 settings = _substitute_values(settings, environ)
85 settings = _substitute_values(settings, environ)
85
86
86 sanitize_settings_and_apply_defaults(settings)
87 sanitize_settings_and_apply_defaults(settings)
87
88
88 config = Configurator(settings=settings)
89 config = Configurator(settings=settings)
89
90
90 # Apply compatibility patches
91 # Apply compatibility patches
91 patches.inspect_getargspec()
92 patches.inspect_getargspec()
92
93
93 load_pyramid_environment(global_config, settings)
94 load_pyramid_environment(global_config, settings)
94
95
95 # Static file view comes first
96 # Static file view comes first
96 includeme_first(config)
97 includeme_first(config)
97
98
98 includeme(config)
99 includeme(config)
99
100
100 pyramid_app = config.make_wsgi_app()
101 pyramid_app = config.make_wsgi_app()
101 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
102 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
102 pyramid_app.config = config
103 pyramid_app.config = config
103
104
104 config.configure_celery(global_config['__file__'])
105 config.configure_celery(global_config['__file__'])
105 # creating the app uses a connection - return it after we are done
106 # creating the app uses a connection - return it after we are done
106 meta.Session.remove()
107 meta.Session.remove()
107
108
108 log.info('Pyramid app %s created and configured.', pyramid_app)
109 log.info('Pyramid app %s created and configured.', pyramid_app)
109 return pyramid_app
110 return pyramid_app
110
111
111
112
112 def not_found_view(request):
113 def not_found_view(request):
113 """
114 """
114 This creates the view which should be registered as not-found-view to
115 This creates the view which should be registered as not-found-view to
115 pyramid.
116 pyramid.
116 """
117 """
117
118
118 if not getattr(request, 'vcs_call', None):
119 if not getattr(request, 'vcs_call', None):
119 # handle like regular case with our error_handler
120 # handle like regular case with our error_handler
120 return error_handler(HTTPNotFound(), request)
121 return error_handler(HTTPNotFound(), request)
121
122
122 # handle not found view as a vcs call
123 # handle not found view as a vcs call
123 settings = request.registry.settings
124 settings = request.registry.settings
124 ae_client = getattr(request, 'ae_client', None)
125 ae_client = getattr(request, 'ae_client', None)
125 vcs_app = VCSMiddleware(
126 vcs_app = VCSMiddleware(
126 HTTPNotFound(), request.registry, settings,
127 HTTPNotFound(), request.registry, settings,
127 appenlight_client=ae_client)
128 appenlight_client=ae_client)
128
129
129 return wsgiapp(vcs_app)(None, request)
130 return wsgiapp(vcs_app)(None, request)
130
131
131
132
132 def error_handler(exception, request):
133 def error_handler(exception, request):
133 import rhodecode
134 import rhodecode
134 from rhodecode.lib import helpers
135 from rhodecode.lib import helpers
135
136
136 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
137 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
137
138
138 base_response = HTTPInternalServerError()
139 base_response = HTTPInternalServerError()
139 # prefer original exception for the response since it may have headers set
140 # prefer original exception for the response since it may have headers set
140 if isinstance(exception, HTTPException):
141 if isinstance(exception, HTTPException):
141 base_response = exception
142 base_response = exception
142 elif isinstance(exception, VCSCommunicationError):
143 elif isinstance(exception, VCSCommunicationError):
143 base_response = VCSServerUnavailable()
144 base_response = VCSServerUnavailable()
144
145
145 if is_http_error(base_response):
146 if is_http_error(base_response):
146 log.exception(
147 log.exception(
147 'error occurred handling this request for path: %s', request.path)
148 'error occurred handling this request for path: %s', request.path)
148
149
149 error_explanation = base_response.explanation or str(base_response)
150 error_explanation = base_response.explanation or str(base_response)
150 if base_response.status_code == 404:
151 if base_response.status_code == 404:
151 error_explanation += " Or you don't have permission to access it."
152 error_explanation += " Or you don't have permission to access it."
152 c = AttributeDict()
153 c = AttributeDict()
153 c.error_message = base_response.status
154 c.error_message = base_response.status
154 c.error_explanation = error_explanation
155 c.error_explanation = error_explanation
155 c.visual = AttributeDict()
156 c.visual = AttributeDict()
156
157
157 c.visual.rhodecode_support_url = (
158 c.visual.rhodecode_support_url = (
158 request.registry.settings.get('rhodecode_support_url') or
159 request.registry.settings.get('rhodecode_support_url') or
159 request.route_url('rhodecode_support')
160 request.route_url('rhodecode_support')
160 )
161 )
161 c.redirect_time = 0
162 c.redirect_time = 0
162 c.rhodecode_name = rhodecode_title
163 c.rhodecode_name = rhodecode_title
163 if not c.rhodecode_name:
164 if not c.rhodecode_name:
164 c.rhodecode_name = 'Rhodecode'
165 c.rhodecode_name = 'Rhodecode'
165
166
166 c.causes = []
167 c.causes = []
167 if is_http_error(base_response):
168 if is_http_error(base_response):
168 c.causes.append('Server is overloaded.')
169 c.causes.append('Server is overloaded.')
169 c.causes.append('Server database connection is lost.')
170 c.causes.append('Server database connection is lost.')
170 c.causes.append('Server expected unhandled error.')
171 c.causes.append('Server expected unhandled error.')
171
172
172 if hasattr(base_response, 'causes'):
173 if hasattr(base_response, 'causes'):
173 c.causes = base_response.causes
174 c.causes = base_response.causes
174
175
175 c.messages = helpers.flash.pop_messages(request=request)
176 c.messages = helpers.flash.pop_messages(request=request)
176
177
177 exc_info = sys.exc_info()
178 exc_info = sys.exc_info()
178 c.exception_id = id(exc_info)
179 c.exception_id = id(exc_info)
179 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
180 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
180 or base_response.status_code > 499
181 or base_response.status_code > 499
181 c.exception_id_url = request.route_url(
182 c.exception_id_url = request.route_url(
182 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
183 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
183
184
184 if c.show_exception_id:
185 if c.show_exception_id:
185 store_exception(c.exception_id, exc_info)
186 store_exception(c.exception_id, exc_info)
186
187
187 response = render_to_response(
188 response = render_to_response(
188 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
189 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
189 response=base_response)
190 response=base_response)
190
191
191 return response
192 return response
192
193
193
194
194 def includeme_first(config):
195 def includeme_first(config):
195 # redirect automatic browser favicon.ico requests to correct place
196 # redirect automatic browser favicon.ico requests to correct place
196 def favicon_redirect(context, request):
197 def favicon_redirect(context, request):
197 return HTTPFound(
198 return HTTPFound(
198 request.static_path('rhodecode:public/images/favicon.ico'))
199 request.static_path('rhodecode:public/images/favicon.ico'))
199
200
200 config.add_view(favicon_redirect, route_name='favicon')
201 config.add_view(favicon_redirect, route_name='favicon')
201 config.add_route('favicon', '/favicon.ico')
202 config.add_route('favicon', '/favicon.ico')
202
203
203 def robots_redirect(context, request):
204 def robots_redirect(context, request):
204 return HTTPFound(
205 return HTTPFound(
205 request.static_path('rhodecode:public/robots.txt'))
206 request.static_path('rhodecode:public/robots.txt'))
206
207
207 config.add_view(robots_redirect, route_name='robots')
208 config.add_view(robots_redirect, route_name='robots')
208 config.add_route('robots', '/robots.txt')
209 config.add_route('robots', '/robots.txt')
209
210
210 config.add_static_view(
211 config.add_static_view(
211 '_static/deform', 'deform:static')
212 '_static/deform', 'deform:static')
212 config.add_static_view(
213 config.add_static_view(
213 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
214 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
214
215
215
216
216 def includeme(config):
217 def includeme(config):
217 settings = config.registry.settings
218 settings = config.registry.settings
218 config.set_request_factory(Request)
219 config.set_request_factory(Request)
219
220
220 # plugin information
221 # plugin information
221 config.registry.rhodecode_plugins = collections.OrderedDict()
222 config.registry.rhodecode_plugins = collections.OrderedDict()
222
223
223 config.add_directive(
224 config.add_directive(
224 'register_rhodecode_plugin', register_rhodecode_plugin)
225 'register_rhodecode_plugin', register_rhodecode_plugin)
225
226
226 config.add_directive('configure_celery', configure_celery)
227 config.add_directive('configure_celery', configure_celery)
227
228
228 if asbool(settings.get('appenlight', 'false')):
229 if asbool(settings.get('appenlight', 'false')):
229 config.include('appenlight_client.ext.pyramid_tween')
230 config.include('appenlight_client.ext.pyramid_tween')
230
231
231 # Includes which are required. The application would fail without them.
232 # Includes which are required. The application would fail without them.
232 config.include('pyramid_mako')
233 config.include('pyramid_mako')
233 config.include('pyramid_beaker')
234 config.include('pyramid_beaker')
234 config.include('rhodecode.lib.caches')
235 config.include('rhodecode.lib.caches')
235 config.include('rhodecode.lib.rc_cache')
236 config.include('rhodecode.lib.rc_cache')
236
237
237 config.include('rhodecode.authentication')
238 config.include('rhodecode.authentication')
238 config.include('rhodecode.integrations')
239 config.include('rhodecode.integrations')
239
240
240 # apps
241 # apps
241 config.include('rhodecode.apps._base')
242 config.include('rhodecode.apps._base')
242 config.include('rhodecode.apps.ops')
243 config.include('rhodecode.apps.ops')
243
244
244 config.include('rhodecode.apps.admin')
245 config.include('rhodecode.apps.admin')
245 config.include('rhodecode.apps.channelstream')
246 config.include('rhodecode.apps.channelstream')
246 config.include('rhodecode.apps.login')
247 config.include('rhodecode.apps.login')
247 config.include('rhodecode.apps.home')
248 config.include('rhodecode.apps.home')
248 config.include('rhodecode.apps.journal')
249 config.include('rhodecode.apps.journal')
249 config.include('rhodecode.apps.repository')
250 config.include('rhodecode.apps.repository')
250 config.include('rhodecode.apps.repo_group')
251 config.include('rhodecode.apps.repo_group')
251 config.include('rhodecode.apps.user_group')
252 config.include('rhodecode.apps.user_group')
252 config.include('rhodecode.apps.search')
253 config.include('rhodecode.apps.search')
253 config.include('rhodecode.apps.user_profile')
254 config.include('rhodecode.apps.user_profile')
254 config.include('rhodecode.apps.user_group_profile')
255 config.include('rhodecode.apps.user_group_profile')
255 config.include('rhodecode.apps.my_account')
256 config.include('rhodecode.apps.my_account')
256 config.include('rhodecode.apps.svn_support')
257 config.include('rhodecode.apps.svn_support')
257 config.include('rhodecode.apps.ssh_support')
258 config.include('rhodecode.apps.ssh_support')
258 config.include('rhodecode.apps.gist')
259 config.include('rhodecode.apps.gist')
259
260
260 config.include('rhodecode.apps.debug_style')
261 config.include('rhodecode.apps.debug_style')
261 config.include('rhodecode.tweens')
262 config.include('rhodecode.tweens')
262 config.include('rhodecode.api')
263 config.include('rhodecode.api')
263
264
264 config.add_route(
265 config.add_route(
265 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
266 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
266
267
267 config.add_translation_dirs('rhodecode:i18n/')
268 config.add_translation_dirs('rhodecode:i18n/')
268 settings['default_locale_name'] = settings.get('lang', 'en')
269 settings['default_locale_name'] = settings.get('lang', 'en')
269
270
270 # Add subscribers.
271 # Add subscribers.
271 config.add_subscriber(inject_app_settings, ApplicationCreated)
272 config.add_subscriber(inject_app_settings,
272 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
273 pyramid.events.ApplicationCreated)
273 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
274 config.add_subscriber(scan_repositories_if_enabled,
274 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
275 pyramid.events.ApplicationCreated)
275
276 config.add_subscriber(write_metadata_if_needed,
277 pyramid.events.ApplicationCreated)
278 config.add_subscriber(write_js_routes_if_enabled,
279 pyramid.events.ApplicationCreated)
276
280
277 # request custom methods
281 # request custom methods
278 config.add_request_method(
282 config.add_request_method(
279 'rhodecode.lib.partial_renderer.get_partial_renderer',
283 'rhodecode.lib.partial_renderer.get_partial_renderer',
280 'get_partial_renderer')
284 'get_partial_renderer')
281
285
282 # Set the authorization policy.
286 # Set the authorization policy.
283 authz_policy = ACLAuthorizationPolicy()
287 authz_policy = ACLAuthorizationPolicy()
284 config.set_authorization_policy(authz_policy)
288 config.set_authorization_policy(authz_policy)
285
289
286 # Set the default renderer for HTML templates to mako.
290 # Set the default renderer for HTML templates to mako.
287 config.add_mako_renderer('.html')
291 config.add_mako_renderer('.html')
288
292
289 config.add_renderer(
293 config.add_renderer(
290 name='json_ext',
294 name='json_ext',
291 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
295 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
292
296
293 # include RhodeCode plugins
297 # include RhodeCode plugins
294 includes = aslist(settings.get('rhodecode.includes', []))
298 includes = aslist(settings.get('rhodecode.includes', []))
295 for inc in includes:
299 for inc in includes:
296 config.include(inc)
300 config.include(inc)
297
301
298 # custom not found view, if our pyramid app doesn't know how to handle
302 # custom not found view, if our pyramid app doesn't know how to handle
299 # the request pass it to potential VCS handling ap
303 # the request pass it to potential VCS handling ap
300 config.add_notfound_view(not_found_view)
304 config.add_notfound_view(not_found_view)
301 if not settings.get('debugtoolbar.enabled', False):
305 if not settings.get('debugtoolbar.enabled', False):
302 # disabled debugtoolbar handle all exceptions via the error_handlers
306 # disabled debugtoolbar handle all exceptions via the error_handlers
303 config.add_view(error_handler, context=Exception)
307 config.add_view(error_handler, context=Exception)
304
308
305 # all errors including 403/404/50X
309 # all errors including 403/404/50X
306 config.add_view(error_handler, context=HTTPError)
310 config.add_view(error_handler, context=HTTPError)
307
311
308
312
309 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
313 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
310 """
314 """
311 Apply outer WSGI middlewares around the application.
315 Apply outer WSGI middlewares around the application.
312 """
316 """
313 registry = config.registry
317 registry = config.registry
314 settings = registry.settings
318 settings = registry.settings
315
319
316 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
320 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
317 pyramid_app = HttpsFixup(pyramid_app, settings)
321 pyramid_app = HttpsFixup(pyramid_app, settings)
318
322
319 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
323 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
320 pyramid_app, settings)
324 pyramid_app, settings)
321 registry.ae_client = _ae_client
325 registry.ae_client = _ae_client
322
326
323 if settings['gzip_responses']:
327 if settings['gzip_responses']:
324 pyramid_app = make_gzip_middleware(
328 pyramid_app = make_gzip_middleware(
325 pyramid_app, settings, compress_level=1)
329 pyramid_app, settings, compress_level=1)
326
330
327 # this should be the outer most middleware in the wsgi stack since
331 # this should be the outer most middleware in the wsgi stack since
328 # middleware like Routes make database calls
332 # middleware like Routes make database calls
329 def pyramid_app_with_cleanup(environ, start_response):
333 def pyramid_app_with_cleanup(environ, start_response):
330 try:
334 try:
331 return pyramid_app(environ, start_response)
335 return pyramid_app(environ, start_response)
332 finally:
336 finally:
333 # Dispose current database session and rollback uncommitted
337 # Dispose current database session and rollback uncommitted
334 # transactions.
338 # transactions.
335 meta.Session.remove()
339 meta.Session.remove()
336
340
337 # In a single threaded mode server, on non sqlite db we should have
341 # In a single threaded mode server, on non sqlite db we should have
338 # '0 Current Checked out connections' at the end of a request,
342 # '0 Current Checked out connections' at the end of a request,
339 # if not, then something, somewhere is leaving a connection open
343 # if not, then something, somewhere is leaving a connection open
340 pool = meta.Base.metadata.bind.engine.pool
344 pool = meta.Base.metadata.bind.engine.pool
341 log.debug('sa pool status: %s', pool.status())
345 log.debug('sa pool status: %s', pool.status())
342 log.debug('Request processing finalized')
346 log.debug('Request processing finalized')
343
347
344 return pyramid_app_with_cleanup
348 return pyramid_app_with_cleanup
345
349
346
350
347 def sanitize_settings_and_apply_defaults(settings):
351 def sanitize_settings_and_apply_defaults(settings):
348 """
352 """
349 Applies settings defaults and does all type conversion.
353 Applies settings defaults and does all type conversion.
350
354
351 We would move all settings parsing and preparation into this place, so that
355 We would move all settings parsing and preparation into this place, so that
352 we have only one place left which deals with this part. The remaining parts
356 we have only one place left which deals with this part. The remaining parts
353 of the application would start to rely fully on well prepared settings.
357 of the application would start to rely fully on well prepared settings.
354
358
355 This piece would later be split up per topic to avoid a big fat monster
359 This piece would later be split up per topic to avoid a big fat monster
356 function.
360 function.
357 """
361 """
358
362
359 settings.setdefault('rhodecode.edition', 'Community Edition')
363 settings.setdefault('rhodecode.edition', 'Community Edition')
360
364
361 if 'mako.default_filters' not in settings:
365 if 'mako.default_filters' not in settings:
362 # set custom default filters if we don't have it defined
366 # set custom default filters if we don't have it defined
363 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
367 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
364 settings['mako.default_filters'] = 'h_filter'
368 settings['mako.default_filters'] = 'h_filter'
365
369
366 if 'mako.directories' not in settings:
370 if 'mako.directories' not in settings:
367 mako_directories = settings.setdefault('mako.directories', [
371 mako_directories = settings.setdefault('mako.directories', [
368 # Base templates of the original application
372 # Base templates of the original application
369 'rhodecode:templates',
373 'rhodecode:templates',
370 ])
374 ])
371 log.debug(
375 log.debug(
372 "Using the following Mako template directories: %s",
376 "Using the following Mako template directories: %s",
373 mako_directories)
377 mako_directories)
374
378
375 # Default includes, possible to change as a user
379 # Default includes, possible to change as a user
376 pyramid_includes = settings.setdefault('pyramid.includes', [
380 pyramid_includes = settings.setdefault('pyramid.includes', [
377 'rhodecode.lib.middleware.request_wrapper',
381 'rhodecode.lib.middleware.request_wrapper',
378 ])
382 ])
379 log.debug(
383 log.debug(
380 "Using the following pyramid.includes: %s",
384 "Using the following pyramid.includes: %s",
381 pyramid_includes)
385 pyramid_includes)
382
386
383 # TODO: johbo: Re-think this, usually the call to config.include
387 # TODO: johbo: Re-think this, usually the call to config.include
384 # should allow to pass in a prefix.
388 # should allow to pass in a prefix.
385 settings.setdefault('rhodecode.api.url', '/_admin/api')
389 settings.setdefault('rhodecode.api.url', '/_admin/api')
386
390
387 # Sanitize generic settings.
391 # Sanitize generic settings.
388 _list_setting(settings, 'default_encoding', 'UTF-8')
392 _list_setting(settings, 'default_encoding', 'UTF-8')
389 _bool_setting(settings, 'is_test', 'false')
393 _bool_setting(settings, 'is_test', 'false')
390 _bool_setting(settings, 'gzip_responses', 'false')
394 _bool_setting(settings, 'gzip_responses', 'false')
391
395
392 # Call split out functions that sanitize settings for each topic.
396 # Call split out functions that sanitize settings for each topic.
393 _sanitize_appenlight_settings(settings)
397 _sanitize_appenlight_settings(settings)
394 _sanitize_vcs_settings(settings)
398 _sanitize_vcs_settings(settings)
395 _sanitize_cache_settings(settings)
399 _sanitize_cache_settings(settings)
396
400
397 # configure instance id
401 # configure instance id
398 config_utils.set_instance_id(settings)
402 config_utils.set_instance_id(settings)
399
403
400 return settings
404 return settings
401
405
402
406
403 def _sanitize_appenlight_settings(settings):
407 def _sanitize_appenlight_settings(settings):
404 _bool_setting(settings, 'appenlight', 'false')
408 _bool_setting(settings, 'appenlight', 'false')
405
409
406
410
407 def _sanitize_vcs_settings(settings):
411 def _sanitize_vcs_settings(settings):
408 """
412 """
409 Applies settings defaults and does type conversion for all VCS related
413 Applies settings defaults and does type conversion for all VCS related
410 settings.
414 settings.
411 """
415 """
412 _string_setting(settings, 'vcs.svn.compatible_version', '')
416 _string_setting(settings, 'vcs.svn.compatible_version', '')
413 _string_setting(settings, 'git_rev_filter', '--all')
417 _string_setting(settings, 'git_rev_filter', '--all')
414 _string_setting(settings, 'vcs.hooks.protocol', 'http')
418 _string_setting(settings, 'vcs.hooks.protocol', 'http')
415 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
419 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
416 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
420 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
417 _string_setting(settings, 'vcs.server', '')
421 _string_setting(settings, 'vcs.server', '')
418 _string_setting(settings, 'vcs.server.log_level', 'debug')
422 _string_setting(settings, 'vcs.server.log_level', 'debug')
419 _string_setting(settings, 'vcs.server.protocol', 'http')
423 _string_setting(settings, 'vcs.server.protocol', 'http')
420 _bool_setting(settings, 'startup.import_repos', 'false')
424 _bool_setting(settings, 'startup.import_repos', 'false')
421 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
425 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
422 _bool_setting(settings, 'vcs.server.enable', 'true')
426 _bool_setting(settings, 'vcs.server.enable', 'true')
423 _bool_setting(settings, 'vcs.start_server', 'false')
427 _bool_setting(settings, 'vcs.start_server', 'false')
424 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
428 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
425 _int_setting(settings, 'vcs.connection_timeout', 3600)
429 _int_setting(settings, 'vcs.connection_timeout', 3600)
426
430
427 # Support legacy values of vcs.scm_app_implementation. Legacy
431 # Support legacy values of vcs.scm_app_implementation. Legacy
428 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
432 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
429 # which is now mapped to 'http'.
433 # which is now mapped to 'http'.
430 scm_app_impl = settings['vcs.scm_app_implementation']
434 scm_app_impl = settings['vcs.scm_app_implementation']
431 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
435 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
432 settings['vcs.scm_app_implementation'] = 'http'
436 settings['vcs.scm_app_implementation'] = 'http'
433
437
434
438
435 def _sanitize_cache_settings(settings):
439 def _sanitize_cache_settings(settings):
436 _string_setting(settings, 'cache_dir',
440 _string_setting(settings, 'cache_dir',
437 os.path.join(tempfile.gettempdir(), 'rc_cache'))
441 os.path.join(tempfile.gettempdir(), 'rc_cache'))
438 # cache_perms
442 # cache_perms
439 _string_setting(
443 _string_setting(
440 settings,
444 settings,
441 'rc_cache.cache_perms.backend',
445 'rc_cache.cache_perms.backend',
442 'dogpile.cache.rc.file_namespace')
446 'dogpile.cache.rc.file_namespace')
443 _int_setting(
447 _int_setting(
444 settings,
448 settings,
445 'rc_cache.cache_perms.expiration_time',
449 'rc_cache.cache_perms.expiration_time',
446 60)
450 60)
447 _string_setting(
451 _string_setting(
448 settings,
452 settings,
449 'rc_cache.cache_perms.arguments.filename',
453 'rc_cache.cache_perms.arguments.filename',
450 os.path.join(tempfile.gettempdir(), 'rc_cache_1'))
454 os.path.join(tempfile.gettempdir(), 'rc_cache_1'))
451
455
452 # cache_repo
456 # cache_repo
453 _string_setting(
457 _string_setting(
454 settings,
458 settings,
455 'rc_cache.cache_repo.backend',
459 'rc_cache.cache_repo.backend',
456 'dogpile.cache.rc.file_namespace')
460 'dogpile.cache.rc.file_namespace')
457 _int_setting(
461 _int_setting(
458 settings,
462 settings,
459 'rc_cache.cache_repo.expiration_time',
463 'rc_cache.cache_repo.expiration_time',
460 60)
464 60)
461 _string_setting(
465 _string_setting(
462 settings,
466 settings,
463 'rc_cache.cache_repo.arguments.filename',
467 'rc_cache.cache_repo.arguments.filename',
464 os.path.join(tempfile.gettempdir(), 'rc_cache_2'))
468 os.path.join(tempfile.gettempdir(), 'rc_cache_2'))
465
469
466 # sql_cache_short
470 # sql_cache_short
467 _string_setting(
471 _string_setting(
468 settings,
472 settings,
469 'rc_cache.sql_cache_short.backend',
473 'rc_cache.sql_cache_short.backend',
470 'dogpile.cache.rc.memory_lru')
474 'dogpile.cache.rc.memory_lru')
471 _int_setting(
475 _int_setting(
472 settings,
476 settings,
473 'rc_cache.sql_cache_short.expiration_time',
477 'rc_cache.sql_cache_short.expiration_time',
474 30)
478 30)
475 _int_setting(
479 _int_setting(
476 settings,
480 settings,
477 'rc_cache.sql_cache_short.max_size',
481 'rc_cache.sql_cache_short.max_size',
478 10000)
482 10000)
479
483
480
484
481 def _int_setting(settings, name, default):
485 def _int_setting(settings, name, default):
482 settings[name] = int(settings.get(name, default))
486 settings[name] = int(settings.get(name, default))
483
487
484
488
485 def _bool_setting(settings, name, default):
489 def _bool_setting(settings, name, default):
486 input_val = settings.get(name, default)
490 input_val = settings.get(name, default)
487 if isinstance(input_val, unicode):
491 if isinstance(input_val, unicode):
488 input_val = input_val.encode('utf8')
492 input_val = input_val.encode('utf8')
489 settings[name] = asbool(input_val)
493 settings[name] = asbool(input_val)
490
494
491
495
492 def _list_setting(settings, name, default):
496 def _list_setting(settings, name, default):
493 raw_value = settings.get(name, default)
497 raw_value = settings.get(name, default)
494
498
495 old_separator = ','
499 old_separator = ','
496 if old_separator in raw_value:
500 if old_separator in raw_value:
497 # If we get a comma separated list, pass it to our own function.
501 # If we get a comma separated list, pass it to our own function.
498 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
502 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
499 else:
503 else:
500 # Otherwise we assume it uses pyramids space/newline separation.
504 # Otherwise we assume it uses pyramids space/newline separation.
501 settings[name] = aslist(raw_value)
505 settings[name] = aslist(raw_value)
502
506
503
507
504 def _string_setting(settings, name, default, lower=True):
508 def _string_setting(settings, name, default, lower=True):
505 value = settings.get(name, default)
509 value = settings.get(name, default)
506 if lower:
510 if lower:
507 value = value.lower()
511 value = value.lower()
508 settings[name] = value
512 settings[name] = value
509
513
510
514
511 def _substitute_values(mapping, substitutions):
515 def _substitute_values(mapping, substitutions):
512 result = {
516 result = {
513 # Note: Cannot use regular replacements, since they would clash
517 # Note: Cannot use regular replacements, since they would clash
514 # with the implementation of ConfigParser. Using "format" instead.
518 # with the implementation of ConfigParser. Using "format" instead.
515 key: value.format(**substitutions)
519 key: value.format(**substitutions)
516 for key, value in mapping.items()
520 for key, value in mapping.items()
517 }
521 }
518 return result
522 return result
@@ -1,76 +1,78 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2018 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20 from pyramid.threadlocal import get_current_registry
20 from pyramid.threadlocal import get_current_registry
21 from rhodecode.events.base import RhodeCodeIntegrationEvent
21 from rhodecode.events.base import RhodeCodeIntegrationEvent
22
22
23
23
24 log = logging.getLogger(__name__)
24 log = logging.getLogger(__name__)
25
25
26
26
27 def trigger(event, registry=None):
27 def trigger(event, registry=None):
28 """
28 """
29 Helper method to send an event. This wraps the pyramid logic to send an
29 Helper method to send an event. This wraps the pyramid logic to send an
30 event.
30 event.
31 """
31 """
32 # For the first step we are using pyramids thread locals here. If the
32 # For the first step we are using pyramids thread locals here. If the
33 # event mechanism works out as a good solution we should think about
33 # event mechanism works out as a good solution we should think about
34 # passing the registry as an argument to get rid of it.
34 # passing the registry as an argument to get rid of it.
35 event_name = event.__class__
36 log.debug('event %s sent for execution', event_name)
35 registry = registry or get_current_registry()
37 registry = registry or get_current_registry()
36 registry.notify(event)
38 registry.notify(event)
37 log.debug('event %s triggered using registry %s', event.__class__, registry)
39 log.debug('event %s triggered using registry %s', event_name, registry)
38
40
39 # Send the events to integrations directly
41 # Send the events to integrations directly
40 from rhodecode.integrations import integrations_event_handler
42 from rhodecode.integrations import integrations_event_handler
41 if isinstance(event, RhodeCodeIntegrationEvent):
43 if isinstance(event, RhodeCodeIntegrationEvent):
42 integrations_event_handler(event)
44 integrations_event_handler(event)
43
45
44
46
45 from rhodecode.events.user import ( # noqa
47 from rhodecode.events.user import ( # noqa
46 UserPreCreate,
48 UserPreCreate,
47 UserPostCreate,
49 UserPostCreate,
48 UserPreUpdate,
50 UserPreUpdate,
49 UserRegistered,
51 UserRegistered,
50 UserPermissionsChange,
52 UserPermissionsChange,
51 )
53 )
52
54
53 from rhodecode.events.repo import ( # noqa
55 from rhodecode.events.repo import ( # noqa
54 RepoEvent,
56 RepoEvent,
55 RepoPreCreateEvent, RepoCreateEvent,
57 RepoPreCreateEvent, RepoCreateEvent,
56 RepoPreDeleteEvent, RepoDeleteEvent,
58 RepoPreDeleteEvent, RepoDeleteEvent,
57 RepoPrePushEvent, RepoPushEvent,
59 RepoPrePushEvent, RepoPushEvent,
58 RepoPrePullEvent, RepoPullEvent,
60 RepoPrePullEvent, RepoPullEvent,
59 )
61 )
60
62
61 from rhodecode.events.repo_group import ( # noqa
63 from rhodecode.events.repo_group import ( # noqa
62 RepoGroupEvent,
64 RepoGroupEvent,
63 RepoGroupCreateEvent,
65 RepoGroupCreateEvent,
64 RepoGroupUpdateEvent,
66 RepoGroupUpdateEvent,
65 RepoGroupDeleteEvent,
67 RepoGroupDeleteEvent,
66 )
68 )
67
69
68 from rhodecode.events.pullrequest import ( # noqa
70 from rhodecode.events.pullrequest import ( # noqa
69 PullRequestEvent,
71 PullRequestEvent,
70 PullRequestCreateEvent,
72 PullRequestCreateEvent,
71 PullRequestUpdateEvent,
73 PullRequestUpdateEvent,
72 PullRequestCommentEvent,
74 PullRequestCommentEvent,
73 PullRequestReviewEvent,
75 PullRequestReviewEvent,
74 PullRequestMergeEvent,
76 PullRequestMergeEvent,
75 PullRequestCloseEvent,
77 PullRequestCloseEvent,
76 )
78 )
@@ -1,264 +1,279 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-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 logging
21 import logging
22 import datetime
22 import datetime
23
23
24 from rhodecode.lib.jsonalchemy import JsonRaw
24 from rhodecode.lib.jsonalchemy import JsonRaw
25 from rhodecode.model import meta
25 from rhodecode.model import meta
26 from rhodecode.model.db import User, UserLog, Repository
26 from rhodecode.model.db import User, UserLog, Repository
27
27
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31 # action as key, and expected action_data as value
31 # action as key, and expected action_data as value
32 ACTIONS_V1 = {
32 ACTIONS_V1 = {
33 'user.login.success': {'user_agent': ''},
33 'user.login.success': {'user_agent': ''},
34 'user.login.failure': {'user_agent': ''},
34 'user.login.failure': {'user_agent': ''},
35 'user.logout': {'user_agent': ''},
35 'user.logout': {'user_agent': ''},
36 'user.register': {},
36 'user.register': {},
37 'user.password.reset_request': {},
37 'user.password.reset_request': {},
38 'user.push': {'user_agent': '', 'commit_ids': []},
38 'user.push': {'user_agent': '', 'commit_ids': []},
39 'user.pull': {'user_agent': ''},
39 'user.pull': {'user_agent': ''},
40
40
41 'user.create': {'data': {}},
41 'user.create': {'data': {}},
42 'user.delete': {'old_data': {}},
42 'user.delete': {'old_data': {}},
43 'user.edit': {'old_data': {}},
43 'user.edit': {'old_data': {}},
44 'user.edit.permissions': {},
44 'user.edit.permissions': {},
45 'user.edit.ip.add': {'ip': {}, 'user': {}},
45 'user.edit.ip.add': {'ip': {}, 'user': {}},
46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
47 'user.edit.token.add': {'token': {}, 'user': {}},
47 'user.edit.token.add': {'token': {}, 'user': {}},
48 'user.edit.token.delete': {'token': {}, 'user': {}},
48 'user.edit.token.delete': {'token': {}, 'user': {}},
49 'user.edit.email.add': {'email': ''},
49 'user.edit.email.add': {'email': ''},
50 'user.edit.email.delete': {'email': ''},
50 'user.edit.email.delete': {'email': ''},
51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
53 'user.edit.password_reset.enabled': {},
53 'user.edit.password_reset.enabled': {},
54 'user.edit.password_reset.disabled': {},
54 'user.edit.password_reset.disabled': {},
55
55
56 'user_group.create': {'data': {}},
56 'user_group.create': {'data': {}},
57 'user_group.delete': {'old_data': {}},
57 'user_group.delete': {'old_data': {}},
58 'user_group.edit': {'old_data': {}},
58 'user_group.edit': {'old_data': {}},
59 'user_group.edit.permissions': {},
59 'user_group.edit.permissions': {},
60 'user_group.edit.member.add': {'user': {}},
60 'user_group.edit.member.add': {'user': {}},
61 'user_group.edit.member.delete': {'user': {}},
61 'user_group.edit.member.delete': {'user': {}},
62
62
63 'repo.create': {'data': {}},
63 'repo.create': {'data': {}},
64 'repo.fork': {'data': {}},
64 'repo.fork': {'data': {}},
65 'repo.edit': {'old_data': {}},
65 'repo.edit': {'old_data': {}},
66 'repo.edit.permissions': {},
66 'repo.edit.permissions': {},
67 'repo.delete': {'old_data': {}},
67 'repo.delete': {'old_data': {}},
68 'repo.commit.strip': {'commit_id': ''},
68 'repo.commit.strip': {'commit_id': ''},
69 'repo.archive.download': {'user_agent': '', 'archive_name': '',
69 'repo.archive.download': {'user_agent': '', 'archive_name': '',
70 'archive_spec': '', 'archive_cached': ''},
70 'archive_spec': '', 'archive_cached': ''},
71 'repo.pull_request.create': '',
71 'repo.pull_request.create': '',
72 'repo.pull_request.edit': '',
72 'repo.pull_request.edit': '',
73 'repo.pull_request.delete': '',
73 'repo.pull_request.delete': '',
74 'repo.pull_request.close': '',
74 'repo.pull_request.close': '',
75 'repo.pull_request.merge': '',
75 'repo.pull_request.merge': '',
76 'repo.pull_request.vote': '',
76 'repo.pull_request.vote': '',
77 'repo.pull_request.comment.create': '',
77 'repo.pull_request.comment.create': '',
78 'repo.pull_request.comment.delete': '',
78 'repo.pull_request.comment.delete': '',
79
79
80 'repo.pull_request.reviewer.add': '',
80 'repo.pull_request.reviewer.add': '',
81 'repo.pull_request.reviewer.delete': '',
81 'repo.pull_request.reviewer.delete': '',
82
82
83 'repo.commit.comment.create': {'data': {}},
83 'repo.commit.comment.create': {'data': {}},
84 'repo.commit.comment.delete': {'data': {}},
84 'repo.commit.comment.delete': {'data': {}},
85 'repo.commit.vote': '',
85 'repo.commit.vote': '',
86
86
87 'repo_group.create': {'data': {}},
87 'repo_group.create': {'data': {}},
88 'repo_group.edit': {'old_data': {}},
88 'repo_group.edit': {'old_data': {}},
89 'repo_group.edit.permissions': {},
89 'repo_group.edit.permissions': {},
90 'repo_group.delete': {'old_data': {}},
90 'repo_group.delete': {'old_data': {}},
91 }
91 }
92 ACTIONS = ACTIONS_V1
92 ACTIONS = ACTIONS_V1
93
93
94 SOURCE_WEB = 'source_web'
94 SOURCE_WEB = 'source_web'
95 SOURCE_API = 'source_api'
95 SOURCE_API = 'source_api'
96
96
97
97
98 class UserWrap(object):
98 class UserWrap(object):
99 """
99 """
100 Fake object used to imitate AuthUser
100 Fake object used to imitate AuthUser
101 """
101 """
102
102
103 def __init__(self, user_id=None, username=None, ip_addr=None):
103 def __init__(self, user_id=None, username=None, ip_addr=None):
104 self.user_id = user_id
104 self.user_id = user_id
105 self.username = username
105 self.username = username
106 self.ip_addr = ip_addr
106 self.ip_addr = ip_addr
107
107
108
108
109 class RepoWrap(object):
109 class RepoWrap(object):
110 """
110 """
111 Fake object used to imitate RepoObject that audit logger requires
111 Fake object used to imitate RepoObject that audit logger requires
112 """
112 """
113
113
114 def __init__(self, repo_id=None, repo_name=None):
114 def __init__(self, repo_id=None, repo_name=None):
115 self.repo_id = repo_id
115 self.repo_id = repo_id
116 self.repo_name = repo_name
116 self.repo_name = repo_name
117
117
118
118
119 def _store_log(action_name, action_data, user_id, username, user_data,
119 def _store_log(action_name, action_data, user_id, username, user_data,
120 ip_address, repository_id, repository_name):
120 ip_address, repository_id, repository_name):
121 user_log = UserLog()
121 user_log = UserLog()
122 user_log.version = UserLog.VERSION_2
122 user_log.version = UserLog.VERSION_2
123
123
124 user_log.action = action_name
124 user_log.action = action_name
125 user_log.action_data = action_data or JsonRaw(u'{}')
125 user_log.action_data = action_data or JsonRaw(u'{}')
126
126
127 user_log.user_ip = ip_address
127 user_log.user_ip = ip_address
128
128
129 user_log.user_id = user_id
129 user_log.user_id = user_id
130 user_log.username = username
130 user_log.username = username
131 user_log.user_data = user_data or JsonRaw(u'{}')
131 user_log.user_data = user_data or JsonRaw(u'{}')
132
132
133 user_log.repository_id = repository_id
133 user_log.repository_id = repository_id
134 user_log.repository_name = repository_name
134 user_log.repository_name = repository_name
135
135
136 user_log.action_date = datetime.datetime.now()
136 user_log.action_date = datetime.datetime.now()
137
137
138 return user_log
138 return user_log
139
139
140
140
141 def store_web(*args, **kwargs):
141 def store_web(*args, **kwargs):
142 if 'action_data' not in kwargs:
142 if 'action_data' not in kwargs:
143 kwargs['action_data'] = {}
143 kwargs['action_data'] = {}
144 kwargs['action_data'].update({
144 kwargs['action_data'].update({
145 'source': SOURCE_WEB
145 'source': SOURCE_WEB
146 })
146 })
147 return store(*args, **kwargs)
147 return store(*args, **kwargs)
148
148
149
149
150 def store_api(*args, **kwargs):
150 def store_api(*args, **kwargs):
151 if 'action_data' not in kwargs:
151 if 'action_data' not in kwargs:
152 kwargs['action_data'] = {}
152 kwargs['action_data'] = {}
153 kwargs['action_data'].update({
153 kwargs['action_data'].update({
154 'source': SOURCE_API
154 'source': SOURCE_API
155 })
155 })
156 return store(*args, **kwargs)
156 return store(*args, **kwargs)
157
157
158
158
159 def store(action, user, action_data=None, user_data=None, ip_addr=None,
159 def store(action, user, action_data=None, user_data=None, ip_addr=None,
160 repo=None, sa_session=None, commit=False):
160 repo=None, sa_session=None, commit=False):
161 """
161 """
162 Audit logger for various actions made by users, typically this
162 Audit logger for various actions made by users, typically this
163 results in a call such::
163 results in a call such::
164
164
165 from rhodecode.lib import audit_logger
165 from rhodecode.lib import audit_logger
166
166
167 audit_logger.store(
167 audit_logger.store(
168 'repo.edit', user=self._rhodecode_user)
168 'repo.edit', user=self._rhodecode_user)
169 audit_logger.store(
169 audit_logger.store(
170 'repo.delete', action_data={'data': repo_data},
170 'repo.delete', action_data={'data': repo_data},
171 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
171 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
172
172
173 # repo action
173 # repo action
174 audit_logger.store(
174 audit_logger.store(
175 'repo.delete',
175 'repo.delete',
176 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
176 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
177 repo=audit_logger.RepoWrap(repo_name='some-repo'))
177 repo=audit_logger.RepoWrap(repo_name='some-repo'))
178
178
179 # repo action, when we know and have the repository object already
179 # repo action, when we know and have the repository object already
180 audit_logger.store(
180 audit_logger.store(
181 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
181 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
182 user=self._rhodecode_user,
182 user=self._rhodecode_user,
183 repo=repo_object)
183 repo=repo_object)
184
184
185 # alternative wrapper to the above
185 # alternative wrapper to the above
186 audit_logger.store_web(
186 audit_logger.store_web(
187 'repo.delete', action_data={},
187 'repo.delete', action_data={},
188 user=self._rhodecode_user,
188 user=self._rhodecode_user,
189 repo=repo_object)
189 repo=repo_object)
190
190
191 # without an user ?
191 # without an user ?
192 audit_logger.store(
192 audit_logger.store(
193 'user.login.failure',
193 'user.login.failure',
194 user=audit_logger.UserWrap(
194 user=audit_logger.UserWrap(
195 username=self.request.params.get('username'),
195 username=self.request.params.get('username'),
196 ip_addr=self.request.remote_addr))
196 ip_addr=self.request.remote_addr))
197
197
198 """
198 """
199 from rhodecode.lib.utils2 import safe_unicode
199 from rhodecode.lib.utils2 import safe_unicode
200 from rhodecode.lib.auth import AuthUser
200 from rhodecode.lib.auth import AuthUser
201
201
202 action_spec = ACTIONS.get(action, None)
202 action_spec = ACTIONS.get(action, None)
203 if action_spec is None:
203 if action_spec is None:
204 raise ValueError('Action `{}` is not supported'.format(action))
204 raise ValueError('Action `{}` is not supported'.format(action))
205
205
206 if not sa_session:
206 if not sa_session:
207 sa_session = meta.Session()
207 sa_session = meta.Session()
208
208
209 try:
209 try:
210 username = getattr(user, 'username', None)
210 username = getattr(user, 'username', None)
211 if not username:
211 if not username:
212 pass
212 pass
213
213
214 user_id = getattr(user, 'user_id', None)
214 user_id = getattr(user, 'user_id', None)
215 if not user_id:
215 if not user_id:
216 # maybe we have username ? Try to figure user_id from username
216 # maybe we have username ? Try to figure user_id from username
217 if username:
217 if username:
218 user_id = getattr(
218 user_id = getattr(
219 User.get_by_username(username), 'user_id', None)
219 User.get_by_username(username), 'user_id', None)
220
220
221 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
221 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
222 if not ip_addr:
222 if not ip_addr:
223 pass
223 pass
224
224
225 if not user_data:
225 if not user_data:
226 # try to get this from the auth user
226 # try to get this from the auth user
227 if isinstance(user, AuthUser):
227 if isinstance(user, AuthUser):
228 user_data = {
228 user_data = {
229 'username': user.username,
229 'username': user.username,
230 'email': user.email,
230 'email': user.email,
231 }
231 }
232
232
233 repository_name = getattr(repo, 'repo_name', None)
233 repository_name = getattr(repo, 'repo_name', None)
234 repository_id = getattr(repo, 'repo_id', None)
234 repository_id = getattr(repo, 'repo_id', None)
235 if not repository_id:
235 if not repository_id:
236 # maybe we have repo_name ? Try to figure repo_id from repo_name
236 # maybe we have repo_name ? Try to figure repo_id from repo_name
237 if repository_name:
237 if repository_name:
238 repository_id = getattr(
238 repository_id = getattr(
239 Repository.get_by_repo_name(repository_name), 'repo_id', None)
239 Repository.get_by_repo_name(repository_name), 'repo_id', None)
240
240
241 action_name = safe_unicode(action)
241 action_name = safe_unicode(action)
242 ip_address = safe_unicode(ip_addr)
242 ip_address = safe_unicode(ip_addr)
243
243
244 user_log = _store_log(
244 with sa_session.no_autoflush:
245 action_name=action_name,
245 update_user_last_activity(sa_session, user_id)
246 action_data=action_data or {},
247 user_id=user_id,
248 username=username,
249 user_data=user_data or {},
250 ip_address=ip_address,
251 repository_id=repository_id,
252 repository_name=repository_name
253 )
254
246
255 sa_session.add(user_log)
247 user_log = _store_log(
256 if commit:
248 action_name=action_name,
257 sa_session.commit()
249 action_data=action_data or {},
250 user_id=user_id,
251 username=username,
252 user_data=user_data or {},
253 ip_address=ip_address,
254 repository_id=repository_id,
255 repository_name=repository_name
256 )
257
258 sa_session.add(user_log)
259
260 if commit:
261 sa_session.commit()
258
262
259 entry_id = user_log.entry_id or ''
263 entry_id = user_log.entry_id or ''
260 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
264 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
261 entry_id, action_name, user_id, username, ip_address)
265 entry_id, action_name, user_id, username, ip_address)
262
266
263 except Exception:
267 except Exception:
264 log.exception('AUDIT: failed to store audit log')
268 log.exception('AUDIT: failed to store audit log')
269
270
271 def update_user_last_activity(sa_session, user_id):
272 _last_activity = datetime.datetime.now()
273 try:
274 sa_session.query(User).filter(User.user_id == user_id).update(
275 {"last_activity": _last_activity})
276 log.debug(
277 'updated user `%s` last activity to:%s', user_id, _last_activity)
278 except Exception:
279 log.exception("Failed last activity update")
@@ -1,659 +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 caches, rc_cache
43 from rhodecode.lib import caches, 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 submited 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 = caches.compute_key_from_params(
80 txn_id = caches.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
525
525 # check user attributes for password change flag
526 # check user attributes for password change flag
526 user_obj = user
527 user_obj = user
527 if user_obj and user_obj.username != User.DEFAULT_USER and \
528 if user_obj and user_obj.username != User.DEFAULT_USER and \
528 user_obj.user_data.get('force_password_change'):
529 user_obj.user_data.get('force_password_change'):
529 reason = 'password change required'
530 reason = 'password change required'
530 log.debug('User not allowed to authenticate, %s', reason)
531 log.debug('User not allowed to authenticate, %s', reason)
531 return HTTPNotAcceptable(reason)(environ, start_response)
532 return HTTPNotAcceptable(reason)(environ, start_response)
532
533
533 # check permissions for this repository
534 # check permissions for this repository
534 perm = self._check_permission(
535 perm = self._check_permission(
535 action, user, self.acl_repo_name, ip_addr,
536 action, user, self.acl_repo_name, ip_addr,
536 plugin, plugin_cache_active, cache_ttl)
537 plugin, plugin_cache_active, cache_ttl)
537 if not perm:
538 if not perm:
538 return HTTPForbidden()(environ, start_response)
539 return HTTPForbidden()(environ, start_response)
540 environ['rc_auth_user_id'] = user_id
539
541
540 # extras are injected into UI object and later available
542 # extras are injected into UI object and later available
541 # in hooks executed by RhodeCode
543 # in hooks executed by RhodeCode
542 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
544 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
543 extras = vcs_operation_context(
545 extras = vcs_operation_context(
544 environ, repo_name=self.acl_repo_name, username=username,
546 environ, repo_name=self.acl_repo_name, username=username,
545 action=action, scm=self.SCM, check_locking=check_locking,
547 action=action, scm=self.SCM, check_locking=check_locking,
546 is_shadow_repo=self.is_shadow_repo
548 is_shadow_repo=self.is_shadow_repo
547 )
549 )
548
550
549 # ======================================================================
551 # ======================================================================
550 # REQUEST HANDLING
552 # REQUEST HANDLING
551 # ======================================================================
553 # ======================================================================
552 repo_path = os.path.join(
554 repo_path = os.path.join(
553 safe_str(self.base_path), safe_str(self.vcs_repo_name))
555 safe_str(self.base_path), safe_str(self.vcs_repo_name))
554 log.debug('Repository path is %s', repo_path)
556 log.debug('Repository path is %s', repo_path)
555
557
556 fix_PATH()
558 fix_PATH()
557
559
558 log.info(
560 log.info(
559 '%s action on %s repo "%s" by "%s" from %s %s',
561 '%s action on %s repo "%s" by "%s" from %s %s',
560 action, self.SCM, safe_str(self.url_repo_name),
562 action, self.SCM, safe_str(self.url_repo_name),
561 safe_str(username), ip_addr, user_agent)
563 safe_str(username), ip_addr, user_agent)
562
564
563 return self._generate_vcs_response(
565 return self._generate_vcs_response(
564 environ, start_response, repo_path, extras, action)
566 environ, start_response, repo_path, extras, action)
565
567
566 @initialize_generator
568 @initialize_generator
567 def _generate_vcs_response(
569 def _generate_vcs_response(
568 self, environ, start_response, repo_path, extras, action):
570 self, environ, start_response, repo_path, extras, action):
569 """
571 """
570 Returns a generator for the response content.
572 Returns a generator for the response content.
571
573
572 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
573 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
574 also handles the locking exceptions which will be triggered when
576 also handles the locking exceptions which will be triggered when
575 the first chunk is produced by the underlying WSGI application.
577 the first chunk is produced by the underlying WSGI application.
576 """
578 """
577 txn_id = ''
579 txn_id = ''
578 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
580 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
579 # 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
580 # 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
581 # it as wsgi.input
583 # it as wsgi.input
582 data = environ['wsgi.input'].read()
584 data = environ['wsgi.input'].read()
583 environ['wsgi.input'] = StringIO(data)
585 environ['wsgi.input'] = StringIO(data)
584 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
586 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
585
587
586 callback_daemon, extras = self._prepare_callback_daemon(
588 callback_daemon, extras = self._prepare_callback_daemon(
587 extras, environ, action, txn_id=txn_id)
589 extras, environ, action, txn_id=txn_id)
588 log.debug('HOOKS extras is %s', extras)
590 log.debug('HOOKS extras is %s', extras)
589
591
590 config = self._create_config(extras, self.acl_repo_name)
592 config = self._create_config(extras, self.acl_repo_name)
591 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)
592 with callback_daemon:
594 with callback_daemon:
593 app.rc_extras = extras
595 app.rc_extras = extras
594
596
595 try:
597 try:
596 response = app(environ, start_response)
598 response = app(environ, start_response)
597 finally:
599 finally:
598 # This statement works together with the decorator
600 # This statement works together with the decorator
599 # "initialize_generator" above. The decorator ensures that
601 # "initialize_generator" above. The decorator ensures that
600 # we hit the first yield statement before the generator is
602 # we hit the first yield statement before the generator is
601 # returned back to the WSGI server. This is needed to
603 # returned back to the WSGI server. This is needed to
602 # ensure that the call to "app" above triggers the
604 # ensure that the call to "app" above triggers the
603 # needed callback to "start_response" before the
605 # needed callback to "start_response" before the
604 # generator is actually used.
606 # generator is actually used.
605 yield "__init__"
607 yield "__init__"
606
608
607 # iter content
609 # iter content
608 for chunk in response:
610 for chunk in response:
609 yield chunk
611 yield chunk
610
612
611 try:
613 try:
612 # invalidate cache on push
614 # invalidate cache on push
613 if action == 'push':
615 if action == 'push':
614 self._invalidate_cache(self.url_repo_name)
616 self._invalidate_cache(self.url_repo_name)
615 finally:
617 finally:
616 meta.Session.remove()
618 meta.Session.remove()
617
619
618 def _get_repository_name(self, environ):
620 def _get_repository_name(self, environ):
619 """Get repository name out of the environmnent
621 """Get repository name out of the environmnent
620
622
621 :param environ: WSGI environment
623 :param environ: WSGI environment
622 """
624 """
623 raise NotImplementedError()
625 raise NotImplementedError()
624
626
625 def _get_action(self, environ):
627 def _get_action(self, environ):
626 """Map request commands into a pull or push command.
628 """Map request commands into a pull or push command.
627
629
628 :param environ: WSGI environment
630 :param environ: WSGI environment
629 """
631 """
630 raise NotImplementedError()
632 raise NotImplementedError()
631
633
632 def _create_wsgi_app(self, repo_path, repo_name, config):
634 def _create_wsgi_app(self, repo_path, repo_name, config):
633 """Return the WSGI app that will finally handle the request."""
635 """Return the WSGI app that will finally handle the request."""
634 raise NotImplementedError()
636 raise NotImplementedError()
635
637
636 def _create_config(self, extras, repo_name):
638 def _create_config(self, extras, repo_name):
637 """Create a safe config representation."""
639 """Create a safe config representation."""
638 raise NotImplementedError()
640 raise NotImplementedError()
639
641
640 def _should_use_callback_daemon(self, extras, environ, action):
642 def _should_use_callback_daemon(self, extras, environ, action):
641 return True
643 return True
642
644
643 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
645 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
644 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
646 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
645 if not self._should_use_callback_daemon(extras, environ, action):
647 if not self._should_use_callback_daemon(extras, environ, action):
646 # disable callback daemon for actions that don't require it
648 # disable callback daemon for actions that don't require it
647 direct_calls = True
649 direct_calls = True
648
650
649 return prepare_callback_daemon(
651 return prepare_callback_daemon(
650 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
652 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
651 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)
652
654
653
655
654 def _should_check_locking(query_string):
656 def _should_check_locking(query_string):
655 # 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
656 # server see all operation on commit; bookmarks, phases and
658 # server see all operation on commit; bookmarks, phases and
657 # obsolescence marker in different transaction, we don't want to check
659 # obsolescence marker in different transaction, we don't want to check
658 # locking on those
660 # locking on those
659 return query_string not in ['cmd=listkeys']
661 return query_string not in ['cmd=listkeys']
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,328 +1,329 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 import io
20 import io
21 import re
21 import re
22 import datetime
22 import datetime
23 import logging
23 import logging
24 import Queue
24 import Queue
25 import subprocess32
25 import subprocess32
26 import os
26 import os
27
27
28
28
29 from dateutil.parser import parse
29 from dateutil.parser import parse
30 from pyramid.i18n import get_localizer
30 from pyramid.i18n import get_localizer
31 from pyramid.threadlocal import get_current_request
31 from pyramid.threadlocal import get_current_request
32 from pyramid.interfaces import IRoutesMapper
32 from pyramid.interfaces import IRoutesMapper
33 from pyramid.settings import asbool
33 from pyramid.settings import asbool
34 from pyramid.path import AssetResolver
34 from pyramid.path import AssetResolver
35 from threading import Thread
35 from threading import Thread
36
36
37 from rhodecode.translation import _ as tsf
37 from rhodecode.translation import _ as tsf
38 from rhodecode.config.jsroutes import generate_jsroutes_content
38 from rhodecode.config.jsroutes import generate_jsroutes_content
39 from rhodecode.lib import auth
39 from rhodecode.lib import auth
40 from rhodecode.lib.base import get_auth_user
40 from rhodecode.lib.base import get_auth_user
41
41
42
42
43 import rhodecode
43 import rhodecode
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 def add_renderer_globals(event):
49 def add_renderer_globals(event):
50 from rhodecode.lib import helpers
50 from rhodecode.lib import helpers
51
51
52 # TODO: When executed in pyramid view context the request is not available
52 # TODO: When executed in pyramid view context the request is not available
53 # in the event. Find a better solution to get the request.
53 # in the event. Find a better solution to get the request.
54 request = event['request'] or get_current_request()
54 request = event['request'] or get_current_request()
55
55
56 # Add Pyramid translation as '_' to context
56 # Add Pyramid translation as '_' to context
57 event['_'] = request.translate
57 event['_'] = request.translate
58 event['_ungettext'] = request.plularize
58 event['_ungettext'] = request.plularize
59 event['h'] = helpers
59 event['h'] = helpers
60
60
61
61
62 def add_localizer(event):
62 def add_localizer(event):
63 request = event.request
63 request = event.request
64 localizer = request.localizer
64 localizer = request.localizer
65
65
66 def auto_translate(*args, **kwargs):
66 def auto_translate(*args, **kwargs):
67 return localizer.translate(tsf(*args, **kwargs))
67 return localizer.translate(tsf(*args, **kwargs))
68
68
69 request.translate = auto_translate
69 request.translate = auto_translate
70 request.plularize = localizer.pluralize
70 request.plularize = localizer.pluralize
71
71
72
72
73 def set_user_lang(event):
73 def set_user_lang(event):
74 request = event.request
74 request = event.request
75 cur_user = getattr(request, 'user', None)
75 cur_user = getattr(request, 'user', None)
76
76
77 if cur_user:
77 if cur_user:
78 user_lang = cur_user.get_instance().user_data.get('language')
78 user_lang = cur_user.get_instance().user_data.get('language')
79 if user_lang:
79 if user_lang:
80 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
80 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
81 event.request._LOCALE_ = user_lang
81 event.request._LOCALE_ = user_lang
82
82
83
83
84 def add_request_user_context(event):
84 def add_request_user_context(event):
85 """
85 """
86 Adds auth user into request context
86 Adds auth user into request context
87 """
87 """
88 request = event.request
88 request = event.request
89 # access req_id as soon as possible
89 # access req_id as soon as possible
90 req_id = request.req_id
90 req_id = request.req_id
91
91
92 if hasattr(request, 'vcs_call'):
92 if hasattr(request, 'vcs_call'):
93 # skip vcs calls
93 # skip vcs calls
94 return
94 return
95
95
96 if hasattr(request, 'rpc_method'):
96 if hasattr(request, 'rpc_method'):
97 # skip api calls
97 # skip api calls
98 return
98 return
99
99
100 auth_user = get_auth_user(request)
100 auth_user = get_auth_user(request)
101 request.user = auth_user
101 request.user = auth_user
102 request.environ['rc_auth_user'] = auth_user
102 request.environ['rc_auth_user'] = auth_user
103 request.environ['rc_auth_user_id'] = auth_user.user_id
103 request.environ['rc_req_id'] = req_id
104 request.environ['rc_req_id'] = req_id
104
105
105
106
106 def inject_app_settings(event):
107 def inject_app_settings(event):
107 settings = event.app.registry.settings
108 settings = event.app.registry.settings
108 # inject info about available permissions
109 # inject info about available permissions
109 auth.set_available_permissions(settings)
110 auth.set_available_permissions(settings)
110
111
111
112
112 def scan_repositories_if_enabled(event):
113 def scan_repositories_if_enabled(event):
113 """
114 """
114 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
115 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
115 does a repository scan if enabled in the settings.
116 does a repository scan if enabled in the settings.
116 """
117 """
117 settings = event.app.registry.settings
118 settings = event.app.registry.settings
118 vcs_server_enabled = settings['vcs.server.enable']
119 vcs_server_enabled = settings['vcs.server.enable']
119 import_on_startup = settings['startup.import_repos']
120 import_on_startup = settings['startup.import_repos']
120 if vcs_server_enabled and import_on_startup:
121 if vcs_server_enabled and import_on_startup:
121 from rhodecode.model.scm import ScmModel
122 from rhodecode.model.scm import ScmModel
122 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
123 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
123 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
124 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
124 repo2db_mapper(repositories, remove_obsolete=False)
125 repo2db_mapper(repositories, remove_obsolete=False)
125
126
126
127
127 def write_metadata_if_needed(event):
128 def write_metadata_if_needed(event):
128 """
129 """
129 Writes upgrade metadata
130 Writes upgrade metadata
130 """
131 """
131 import rhodecode
132 import rhodecode
132 from rhodecode.lib import system_info
133 from rhodecode.lib import system_info
133 from rhodecode.lib import ext_json
134 from rhodecode.lib import ext_json
134
135
135 fname = '.rcmetadata.json'
136 fname = '.rcmetadata.json'
136 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
137 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
137 metadata_destination = os.path.join(ini_loc, fname)
138 metadata_destination = os.path.join(ini_loc, fname)
138
139
139 def get_update_age():
140 def get_update_age():
140 now = datetime.datetime.utcnow()
141 now = datetime.datetime.utcnow()
141
142
142 with open(metadata_destination, 'rb') as f:
143 with open(metadata_destination, 'rb') as f:
143 data = ext_json.json.loads(f.read())
144 data = ext_json.json.loads(f.read())
144 if 'created_on' in data:
145 if 'created_on' in data:
145 update_date = parse(data['created_on'])
146 update_date = parse(data['created_on'])
146 diff = now - update_date
147 diff = now - update_date
147 return diff.total_seconds() / 60.0
148 return diff.total_seconds() / 60.0
148
149
149 return 0
150 return 0
150
151
151 def write():
152 def write():
152 configuration = system_info.SysInfo(
153 configuration = system_info.SysInfo(
153 system_info.rhodecode_config)()['value']
154 system_info.rhodecode_config)()['value']
154 license_token = configuration['config']['license_token']
155 license_token = configuration['config']['license_token']
155
156
156 setup = dict(
157 setup = dict(
157 workers=configuration['config']['server:main'].get(
158 workers=configuration['config']['server:main'].get(
158 'workers', '?'),
159 'workers', '?'),
159 worker_type=configuration['config']['server:main'].get(
160 worker_type=configuration['config']['server:main'].get(
160 'worker_class', 'sync'),
161 'worker_class', 'sync'),
161 )
162 )
162 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
163 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
163 del dbinfo['url']
164 del dbinfo['url']
164
165
165 metadata = dict(
166 metadata = dict(
166 desc='upgrade metadata info',
167 desc='upgrade metadata info',
167 license_token=license_token,
168 license_token=license_token,
168 created_on=datetime.datetime.utcnow().isoformat(),
169 created_on=datetime.datetime.utcnow().isoformat(),
169 usage=system_info.SysInfo(system_info.usage_info)()['value'],
170 usage=system_info.SysInfo(system_info.usage_info)()['value'],
170 platform=system_info.SysInfo(system_info.platform_type)()['value'],
171 platform=system_info.SysInfo(system_info.platform_type)()['value'],
171 database=dbinfo,
172 database=dbinfo,
172 cpu=system_info.SysInfo(system_info.cpu)()['value'],
173 cpu=system_info.SysInfo(system_info.cpu)()['value'],
173 memory=system_info.SysInfo(system_info.memory)()['value'],
174 memory=system_info.SysInfo(system_info.memory)()['value'],
174 setup=setup
175 setup=setup
175 )
176 )
176
177
177 with open(metadata_destination, 'wb') as f:
178 with open(metadata_destination, 'wb') as f:
178 f.write(ext_json.json.dumps(metadata))
179 f.write(ext_json.json.dumps(metadata))
179
180
180 settings = event.app.registry.settings
181 settings = event.app.registry.settings
181 if settings.get('metadata.skip'):
182 if settings.get('metadata.skip'):
182 return
183 return
183
184
184 # only write this every 24h, workers restart caused unwanted delays
185 # only write this every 24h, workers restart caused unwanted delays
185 try:
186 try:
186 age_in_min = get_update_age()
187 age_in_min = get_update_age()
187 except Exception:
188 except Exception:
188 age_in_min = 0
189 age_in_min = 0
189
190
190 if age_in_min > 60 * 60 * 24:
191 if age_in_min > 60 * 60 * 24:
191 return
192 return
192
193
193 try:
194 try:
194 write()
195 write()
195 except Exception:
196 except Exception:
196 pass
197 pass
197
198
198
199
199 def write_js_routes_if_enabled(event):
200 def write_js_routes_if_enabled(event):
200 registry = event.app.registry
201 registry = event.app.registry
201
202
202 mapper = registry.queryUtility(IRoutesMapper)
203 mapper = registry.queryUtility(IRoutesMapper)
203 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
204 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
204
205
205 def _extract_route_information(route):
206 def _extract_route_information(route):
206 """
207 """
207 Convert a route into tuple(name, path, args), eg:
208 Convert a route into tuple(name, path, args), eg:
208 ('show_user', '/profile/%(username)s', ['username'])
209 ('show_user', '/profile/%(username)s', ['username'])
209 """
210 """
210
211
211 routepath = route.pattern
212 routepath = route.pattern
212 pattern = route.pattern
213 pattern = route.pattern
213
214
214 def replace(matchobj):
215 def replace(matchobj):
215 if matchobj.group(1):
216 if matchobj.group(1):
216 return "%%(%s)s" % matchobj.group(1).split(':')[0]
217 return "%%(%s)s" % matchobj.group(1).split(':')[0]
217 else:
218 else:
218 return "%%(%s)s" % matchobj.group(2)
219 return "%%(%s)s" % matchobj.group(2)
219
220
220 routepath = _argument_prog.sub(replace, routepath)
221 routepath = _argument_prog.sub(replace, routepath)
221
222
222 if not routepath.startswith('/'):
223 if not routepath.startswith('/'):
223 routepath = '/'+routepath
224 routepath = '/'+routepath
224
225
225 return (
226 return (
226 route.name,
227 route.name,
227 routepath,
228 routepath,
228 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
229 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
229 for arg in _argument_prog.findall(pattern)]
230 for arg in _argument_prog.findall(pattern)]
230 )
231 )
231
232
232 def get_routes():
233 def get_routes():
233 # pyramid routes
234 # pyramid routes
234 for route in mapper.get_routes():
235 for route in mapper.get_routes():
235 if not route.name.startswith('__'):
236 if not route.name.startswith('__'):
236 yield _extract_route_information(route)
237 yield _extract_route_information(route)
237
238
238 if asbool(registry.settings.get('generate_js_files', 'false')):
239 if asbool(registry.settings.get('generate_js_files', 'false')):
239 static_path = AssetResolver().resolve('rhodecode:public').abspath()
240 static_path = AssetResolver().resolve('rhodecode:public').abspath()
240 jsroutes = get_routes()
241 jsroutes = get_routes()
241 jsroutes_file_content = generate_jsroutes_content(jsroutes)
242 jsroutes_file_content = generate_jsroutes_content(jsroutes)
242 jsroutes_file_path = os.path.join(
243 jsroutes_file_path = os.path.join(
243 static_path, 'js', 'rhodecode', 'routes.js')
244 static_path, 'js', 'rhodecode', 'routes.js')
244
245
245 try:
246 try:
246 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
247 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
247 f.write(jsroutes_file_content)
248 f.write(jsroutes_file_content)
248 except Exception:
249 except Exception:
249 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
250 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
250
251
251
252
252 class Subscriber(object):
253 class Subscriber(object):
253 """
254 """
254 Base class for subscribers to the pyramid event system.
255 Base class for subscribers to the pyramid event system.
255 """
256 """
256 def __call__(self, event):
257 def __call__(self, event):
257 self.run(event)
258 self.run(event)
258
259
259 def run(self, event):
260 def run(self, event):
260 raise NotImplementedError('Subclass has to implement this.')
261 raise NotImplementedError('Subclass has to implement this.')
261
262
262
263
263 class AsyncSubscriber(Subscriber):
264 class AsyncSubscriber(Subscriber):
264 """
265 """
265 Subscriber that handles the execution of events in a separate task to not
266 Subscriber that handles the execution of events in a separate task to not
266 block the execution of the code which triggers the event. It puts the
267 block the execution of the code which triggers the event. It puts the
267 received events into a queue from which the worker process takes them in
268 received events into a queue from which the worker process takes them in
268 order.
269 order.
269 """
270 """
270 def __init__(self):
271 def __init__(self):
271 self._stop = False
272 self._stop = False
272 self._eventq = Queue.Queue()
273 self._eventq = Queue.Queue()
273 self._worker = self.create_worker()
274 self._worker = self.create_worker()
274 self._worker.start()
275 self._worker.start()
275
276
276 def __call__(self, event):
277 def __call__(self, event):
277 self._eventq.put(event)
278 self._eventq.put(event)
278
279
279 def create_worker(self):
280 def create_worker(self):
280 worker = Thread(target=self.do_work)
281 worker = Thread(target=self.do_work)
281 worker.daemon = True
282 worker.daemon = True
282 return worker
283 return worker
283
284
284 def stop_worker(self):
285 def stop_worker(self):
285 self._stop = False
286 self._stop = False
286 self._eventq.put(None)
287 self._eventq.put(None)
287 self._worker.join()
288 self._worker.join()
288
289
289 def do_work(self):
290 def do_work(self):
290 while not self._stop:
291 while not self._stop:
291 event = self._eventq.get()
292 event = self._eventq.get()
292 if event is not None:
293 if event is not None:
293 self.run(event)
294 self.run(event)
294
295
295
296
296 class AsyncSubprocessSubscriber(AsyncSubscriber):
297 class AsyncSubprocessSubscriber(AsyncSubscriber):
297 """
298 """
298 Subscriber that uses the subprocess32 module to execute a command if an
299 Subscriber that uses the subprocess32 module to execute a command if an
299 event is received. Events are handled asynchronously.
300 event is received. Events are handled asynchronously.
300 """
301 """
301
302
302 def __init__(self, cmd, timeout=None):
303 def __init__(self, cmd, timeout=None):
303 super(AsyncSubprocessSubscriber, self).__init__()
304 super(AsyncSubprocessSubscriber, self).__init__()
304 self._cmd = cmd
305 self._cmd = cmd
305 self._timeout = timeout
306 self._timeout = timeout
306
307
307 def run(self, event):
308 def run(self, event):
308 cmd = self._cmd
309 cmd = self._cmd
309 timeout = self._timeout
310 timeout = self._timeout
310 log.debug('Executing command %s.', cmd)
311 log.debug('Executing command %s.', cmd)
311
312
312 try:
313 try:
313 output = subprocess32.check_output(
314 output = subprocess32.check_output(
314 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
315 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
315 log.debug('Command finished %s', cmd)
316 log.debug('Command finished %s', cmd)
316 if output:
317 if output:
317 log.debug('Command output: %s', output)
318 log.debug('Command output: %s', output)
318 except subprocess32.TimeoutExpired as e:
319 except subprocess32.TimeoutExpired as e:
319 log.exception('Timeout while executing command.')
320 log.exception('Timeout while executing command.')
320 if e.output:
321 if e.output:
321 log.error('Command output: %s', e.output)
322 log.error('Command output: %s', e.output)
322 except subprocess32.CalledProcessError as e:
323 except subprocess32.CalledProcessError as e:
323 log.exception('Error while executing command.')
324 log.exception('Error while executing command.')
324 if e.output:
325 if e.output:
325 log.error('Command output: %s', e.output)
326 log.error('Command output: %s', e.output)
326 except:
327 except:
327 log.exception(
328 log.exception(
328 'Exception while executing command %s.', cmd)
329 'Exception while executing command %s.', cmd)
General Comments 0
You need to be logged in to leave comments. Login now