##// END OF EJS Templates
fix(api.url): set default api.url and re-use defaults in ssh wrappers
super-admin -
r5317:688c5949 default
parent child Browse files
Show More
@@ -1,582 +1,581 b''
1 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 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 itertools
19 import itertools
20 import logging
20 import logging
21 import sys
21 import sys
22 import fnmatch
22 import fnmatch
23
23
24 import decorator
24 import decorator
25 import typing
26 import venusian
25 import venusian
27 from collections import OrderedDict
26 from collections import OrderedDict
28
27
29 from pyramid.exceptions import ConfigurationError
28 from pyramid.exceptions import ConfigurationError
30 from pyramid.renderers import render
29 from pyramid.renderers import render
31 from pyramid.response import Response
30 from pyramid.response import Response
32 from pyramid.httpexceptions import HTTPNotFound
31 from pyramid.httpexceptions import HTTPNotFound
33
32
34 from rhodecode.api.exc import (
33 from rhodecode.api.exc import (
35 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
34 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
36 from rhodecode.apps._base import TemplateArgs
35 from rhodecode.apps._base import TemplateArgs
37 from rhodecode.lib.auth import AuthUser
36 from rhodecode.lib.auth import AuthUser
38 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
37 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
39 from rhodecode.lib.exc_tracking import store_exception
38 from rhodecode.lib.exc_tracking import store_exception
40 from rhodecode.lib import ext_json
39 from rhodecode.lib import ext_json
41 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.lib.plugins.utils import get_plugin_settings
41 from rhodecode.lib.plugins.utils import get_plugin_settings
43 from rhodecode.model.db import User, UserApiKeys
42 from rhodecode.model.db import User, UserApiKeys
44
43
45 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
46
45
47 DEFAULT_RENDERER = 'jsonrpc_renderer'
46 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 DEFAULT_URL = '/_admin/apiv2'
47 DEFAULT_URL = '/_admin/api'
49 SERVICE_API_IDENTIFIER = 'service_'
48 SERVICE_API_IDENTIFIER = 'service_'
50
49
51
50
52 def find_methods(jsonrpc_methods, pattern):
51 def find_methods(jsonrpc_methods, pattern):
53 matches = OrderedDict()
52 matches = OrderedDict()
54 if not isinstance(pattern, (list, tuple)):
53 if not isinstance(pattern, (list, tuple)):
55 pattern = [pattern]
54 pattern = [pattern]
56
55
57 for single_pattern in pattern:
56 for single_pattern in pattern:
58 for method_name, method in filter(
57 for method_name, method in filter(
59 lambda x: not x[0].startswith(SERVICE_API_IDENTIFIER), jsonrpc_methods.items()
58 lambda x: not x[0].startswith(SERVICE_API_IDENTIFIER), jsonrpc_methods.items()
60 ):
59 ):
61 if fnmatch.fnmatch(method_name, single_pattern):
60 if fnmatch.fnmatch(method_name, single_pattern):
62 matches[method_name] = method
61 matches[method_name] = method
63 return matches
62 return matches
64
63
65
64
66 class ExtJsonRenderer(object):
65 class ExtJsonRenderer(object):
67 """
66 """
68 Custom renderer that makes use of our ext_json lib
67 Custom renderer that makes use of our ext_json lib
69
68
70 """
69 """
71
70
72 def __init__(self):
71 def __init__(self):
73 self.serializer = ext_json.formatted_json
72 self.serializer = ext_json.formatted_json
74
73
75 def __call__(self, info):
74 def __call__(self, info):
76 """ Returns a plain JSON-encoded string with content-type
75 """ Returns a plain JSON-encoded string with content-type
77 ``application/json``. The content-type may be overridden by
76 ``application/json``. The content-type may be overridden by
78 setting ``request.response.content_type``."""
77 setting ``request.response.content_type``."""
79
78
80 def _render(value, system):
79 def _render(value, system):
81 request = system.get('request')
80 request = system.get('request')
82 if request is not None:
81 if request is not None:
83 response = request.response
82 response = request.response
84 ct = response.content_type
83 ct = response.content_type
85 if ct == response.default_content_type:
84 if ct == response.default_content_type:
86 response.content_type = 'application/json'
85 response.content_type = 'application/json'
87
86
88 return self.serializer(value)
87 return self.serializer(value)
89
88
90 return _render
89 return _render
91
90
92
91
93 def jsonrpc_response(request, result):
92 def jsonrpc_response(request, result):
94 rpc_id = getattr(request, 'rpc_id', None)
93 rpc_id = getattr(request, 'rpc_id', None)
95
94
96 ret_value = ''
95 ret_value = ''
97 if rpc_id:
96 if rpc_id:
98 ret_value = {'id': rpc_id, 'result': result, 'error': None}
97 ret_value = {'id': rpc_id, 'result': result, 'error': None}
99
98
100 # fetch deprecation warnings, and store it inside results
99 # fetch deprecation warnings, and store it inside results
101 deprecation = getattr(request, 'rpc_deprecation', None)
100 deprecation = getattr(request, 'rpc_deprecation', None)
102 if deprecation:
101 if deprecation:
103 ret_value['DEPRECATION_WARNING'] = deprecation
102 ret_value['DEPRECATION_WARNING'] = deprecation
104
103
105 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
104 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
106 content_type = 'application/json'
105 content_type = 'application/json'
107 content_type_header = 'Content-Type'
106 content_type_header = 'Content-Type'
108 headers = {
107 headers = {
109 content_type_header: content_type
108 content_type_header: content_type
110 }
109 }
111 return Response(
110 return Response(
112 body=raw_body,
111 body=raw_body,
113 content_type=content_type,
112 content_type=content_type,
114 headerlist=[(k, v) for k, v in headers.items()]
113 headerlist=[(k, v) for k, v in headers.items()]
115 )
114 )
116
115
117
116
118 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
117 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
119 """
118 """
120 Generate a Response object with a JSON-RPC error body
119 Generate a Response object with a JSON-RPC error body
121 """
120 """
122 headers = headers or {}
121 headers = headers or {}
123 content_type = 'application/json'
122 content_type = 'application/json'
124 content_type_header = 'Content-Type'
123 content_type_header = 'Content-Type'
125 if content_type_header not in headers:
124 if content_type_header not in headers:
126 headers[content_type_header] = content_type
125 headers[content_type_header] = content_type
127
126
128 err_dict = {'id': retid, 'result': None, 'error': message}
127 err_dict = {'id': retid, 'result': None, 'error': message}
129 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
128 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
130
129
131 return Response(
130 return Response(
132 body=raw_body,
131 body=raw_body,
133 status=code,
132 status=code,
134 content_type=content_type,
133 content_type=content_type,
135 headerlist=[(k, v) for k, v in headers.items()]
134 headerlist=[(k, v) for k, v in headers.items()]
136 )
135 )
137
136
138
137
139 def exception_view(exc, request):
138 def exception_view(exc, request):
140 rpc_id = getattr(request, 'rpc_id', None)
139 rpc_id = getattr(request, 'rpc_id', None)
141
140
142 if isinstance(exc, JSONRPCError):
141 if isinstance(exc, JSONRPCError):
143 fault_message = safe_str(exc)
142 fault_message = safe_str(exc)
144 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
143 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
145 elif isinstance(exc, JSONRPCValidationError):
144 elif isinstance(exc, JSONRPCValidationError):
146 colander_exc = exc.colander_exception
145 colander_exc = exc.colander_exception
147 # TODO(marcink): think maybe of nicer way to serialize errors ?
146 # TODO(marcink): think maybe of nicer way to serialize errors ?
148 fault_message = colander_exc.asdict()
147 fault_message = colander_exc.asdict()
149 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
148 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
150 elif isinstance(exc, JSONRPCForbidden):
149 elif isinstance(exc, JSONRPCForbidden):
151 fault_message = 'Access was denied to this resource.'
150 fault_message = 'Access was denied to this resource.'
152 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
151 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
153 elif isinstance(exc, HTTPNotFound):
152 elif isinstance(exc, HTTPNotFound):
154 method = request.rpc_method
153 method = request.rpc_method
155 log.debug('json-rpc method `%s` not found in list of '
154 log.debug('json-rpc method `%s` not found in list of '
156 'api calls: %s, rpc_id:%s',
155 'api calls: %s, rpc_id:%s',
157 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
156 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
158
157
159 similar = 'none'
158 similar = 'none'
160 try:
159 try:
161 similar_paterns = [f'*{x}*' for x in method.split('_')]
160 similar_paterns = [f'*{x}*' for x in method.split('_')]
162 similar_found = find_methods(
161 similar_found = find_methods(
163 request.registry.jsonrpc_methods, similar_paterns)
162 request.registry.jsonrpc_methods, similar_paterns)
164 similar = ', '.join(similar_found.keys()) or similar
163 similar = ', '.join(similar_found.keys()) or similar
165 except Exception:
164 except Exception:
166 # make the whole above block safe
165 # make the whole above block safe
167 pass
166 pass
168
167
169 fault_message = f"No such method: {method}. Similar methods: {similar}"
168 fault_message = f"No such method: {method}. Similar methods: {similar}"
170 else:
169 else:
171 fault_message = 'undefined error'
170 fault_message = 'undefined error'
172 exc_info = exc.exc_info()
171 exc_info = exc.exc_info()
173 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
172 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
174
173
175 statsd = request.registry.statsd
174 statsd = request.registry.statsd
176 if statsd:
175 if statsd:
177 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
176 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
178 statsd.incr('rhodecode_exception_total',
177 statsd.incr('rhodecode_exception_total',
179 tags=["exc_source:api", f"type:{exc_type}"])
178 tags=["exc_source:api", f"type:{exc_type}"])
180
179
181 return jsonrpc_error(request, fault_message, rpc_id)
180 return jsonrpc_error(request, fault_message, rpc_id)
182
181
183
182
184 def request_view(request):
183 def request_view(request):
185 """
184 """
186 Main request handling method. It handles all logic to call a specific
185 Main request handling method. It handles all logic to call a specific
187 exposed method
186 exposed method
188 """
187 """
189 # cython compatible inspect
188 # cython compatible inspect
190 from rhodecode.config.patches import inspect_getargspec
189 from rhodecode.config.patches import inspect_getargspec
191 inspect = inspect_getargspec()
190 inspect = inspect_getargspec()
192
191
193 # check if we can find this session using api_key, get_by_auth_token
192 # check if we can find this session using api_key, get_by_auth_token
194 # search not expired tokens only
193 # search not expired tokens only
195 try:
194 try:
196 if not request.rpc_method.startswith(SERVICE_API_IDENTIFIER):
195 if not request.rpc_method.startswith(SERVICE_API_IDENTIFIER):
197 api_user = User.get_by_auth_token(request.rpc_api_key)
196 api_user = User.get_by_auth_token(request.rpc_api_key)
198
197
199 if api_user is None:
198 if api_user is None:
200 return jsonrpc_error(
199 return jsonrpc_error(
201 request, retid=request.rpc_id, message='Invalid API KEY')
200 request, retid=request.rpc_id, message='Invalid API KEY')
202
201
203 if not api_user.active:
202 if not api_user.active:
204 return jsonrpc_error(
203 return jsonrpc_error(
205 request, retid=request.rpc_id,
204 request, retid=request.rpc_id,
206 message='Request from this user not allowed')
205 message='Request from this user not allowed')
207
206
208 # check if we are allowed to use this IP
207 # check if we are allowed to use this IP
209 auth_u = AuthUser(
208 auth_u = AuthUser(
210 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
209 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
211 if not auth_u.ip_allowed:
210 if not auth_u.ip_allowed:
212 return jsonrpc_error(
211 return jsonrpc_error(
213 request, retid=request.rpc_id,
212 request, retid=request.rpc_id,
214 message='Request from IP:{} not allowed'.format(
213 message='Request from IP:{} not allowed'.format(
215 request.rpc_ip_addr))
214 request.rpc_ip_addr))
216 else:
215 else:
217 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
216 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
218
217
219 # register our auth-user
218 # register our auth-user
220 request.rpc_user = auth_u
219 request.rpc_user = auth_u
221 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
220 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
222
221
223 # now check if token is valid for API
222 # now check if token is valid for API
224 auth_token = request.rpc_api_key
223 auth_token = request.rpc_api_key
225 token_match = api_user.authenticate_by_token(
224 token_match = api_user.authenticate_by_token(
226 auth_token, roles=[UserApiKeys.ROLE_API])
225 auth_token, roles=[UserApiKeys.ROLE_API])
227 invalid_token = not token_match
226 invalid_token = not token_match
228
227
229 log.debug('Checking if API KEY is valid with proper role')
228 log.debug('Checking if API KEY is valid with proper role')
230 if invalid_token:
229 if invalid_token:
231 return jsonrpc_error(
230 return jsonrpc_error(
232 request, retid=request.rpc_id,
231 request, retid=request.rpc_id,
233 message='API KEY invalid or, has bad role for an API call')
232 message='API KEY invalid or, has bad role for an API call')
234 else:
233 else:
235 auth_u = 'service'
234 auth_u = 'service'
236 if request.rpc_api_key != request.registry.settings['app.service_api.token']:
235 if request.rpc_api_key != request.registry.settings['app.service_api.token']:
237 raise Exception("Provided service secret is not recognized!")
236 raise Exception("Provided service secret is not recognized!")
238
237
239 except Exception:
238 except Exception:
240 log.exception('Error on API AUTH')
239 log.exception('Error on API AUTH')
241 return jsonrpc_error(
240 return jsonrpc_error(
242 request, retid=request.rpc_id, message='Invalid API KEY')
241 request, retid=request.rpc_id, message='Invalid API KEY')
243
242
244 method = request.rpc_method
243 method = request.rpc_method
245 func = request.registry.jsonrpc_methods[method]
244 func = request.registry.jsonrpc_methods[method]
246
245
247 # now that we have a method, add request._req_params to
246 # now that we have a method, add request._req_params to
248 # self.kargs and dispatch control to WGIController
247 # self.kargs and dispatch control to WGIController
249
248
250 argspec = inspect.getargspec(func)
249 argspec = inspect.getargspec(func)
251 arglist = argspec[0]
250 arglist = argspec[0]
252 defs = argspec[3] or []
251 defs = argspec[3] or []
253 defaults = [type(a) for a in defs]
252 defaults = [type(a) for a in defs]
254 default_empty = type(NotImplemented)
253 default_empty = type(NotImplemented)
255
254
256 # kw arguments required by this method
255 # kw arguments required by this method
257 func_kwargs = dict(itertools.zip_longest(
256 func_kwargs = dict(itertools.zip_longest(
258 reversed(arglist), reversed(defaults), fillvalue=default_empty))
257 reversed(arglist), reversed(defaults), fillvalue=default_empty))
259
258
260 # This attribute will need to be first param of a method that uses
259 # This attribute will need to be first param of a method that uses
261 # api_key, which is translated to instance of user at that name
260 # api_key, which is translated to instance of user at that name
262 user_var = 'apiuser'
261 user_var = 'apiuser'
263 request_var = 'request'
262 request_var = 'request'
264
263
265 for arg in [user_var, request_var]:
264 for arg in [user_var, request_var]:
266 if arg not in arglist:
265 if arg not in arglist:
267 return jsonrpc_error(
266 return jsonrpc_error(
268 request,
267 request,
269 retid=request.rpc_id,
268 retid=request.rpc_id,
270 message='This method [%s] does not support '
269 message='This method [%s] does not support '
271 'required parameter `%s`' % (func.__name__, arg))
270 'required parameter `%s`' % (func.__name__, arg))
272
271
273 # get our arglist and check if we provided them as args
272 # get our arglist and check if we provided them as args
274 for arg, default in func_kwargs.items():
273 for arg, default in func_kwargs.items():
275 if arg in [user_var, request_var]:
274 if arg in [user_var, request_var]:
276 # user_var and request_var are pre-hardcoded parameters and we
275 # user_var and request_var are pre-hardcoded parameters and we
277 # don't need to do any translation
276 # don't need to do any translation
278 continue
277 continue
279
278
280 # skip the required param check if it's default value is
279 # skip the required param check if it's default value is
281 # NotImplementedType (default_empty)
280 # NotImplementedType (default_empty)
282 if default == default_empty and arg not in request.rpc_params:
281 if default == default_empty and arg not in request.rpc_params:
283 return jsonrpc_error(
282 return jsonrpc_error(
284 request,
283 request,
285 retid=request.rpc_id,
284 retid=request.rpc_id,
286 message=('Missing non optional `%s` arg in JSON DATA' % arg)
285 message=('Missing non optional `%s` arg in JSON DATA' % arg)
287 )
286 )
288
287
289 # sanitize extra passed arguments
288 # sanitize extra passed arguments
290 for k in list(request.rpc_params.keys()):
289 for k in list(request.rpc_params.keys()):
291 if k not in func_kwargs:
290 if k not in func_kwargs:
292 del request.rpc_params[k]
291 del request.rpc_params[k]
293
292
294 call_params = request.rpc_params
293 call_params = request.rpc_params
295 call_params.update({
294 call_params.update({
296 'request': request,
295 'request': request,
297 'apiuser': auth_u
296 'apiuser': auth_u
298 })
297 })
299
298
300 # register some common functions for usage
299 # register some common functions for usage
301 rpc_user = request.rpc_user.user_id if hasattr(request, 'rpc_user') else None
300 rpc_user = request.rpc_user.user_id if hasattr(request, 'rpc_user') else None
302 attach_context_attributes(TemplateArgs(), request, rpc_user)
301 attach_context_attributes(TemplateArgs(), request, rpc_user)
303
302
304 statsd = request.registry.statsd
303 statsd = request.registry.statsd
305
304
306 try:
305 try:
307 ret_value = func(**call_params)
306 ret_value = func(**call_params)
308 resp = jsonrpc_response(request, ret_value)
307 resp = jsonrpc_response(request, ret_value)
309 if statsd:
308 if statsd:
310 statsd.incr('rhodecode_api_call_success_total')
309 statsd.incr('rhodecode_api_call_success_total')
311 return resp
310 return resp
312 except JSONRPCBaseError:
311 except JSONRPCBaseError:
313 raise
312 raise
314 except Exception:
313 except Exception:
315 log.exception('Unhandled exception occurred on api call: %s', func)
314 log.exception('Unhandled exception occurred on api call: %s', func)
316 exc_info = sys.exc_info()
315 exc_info = sys.exc_info()
317 exc_id, exc_type_name = store_exception(
316 exc_id, exc_type_name = store_exception(
318 id(exc_info), exc_info, prefix='rhodecode-api')
317 id(exc_info), exc_info, prefix='rhodecode-api')
319 error_headers = {
318 error_headers = {
320 'RhodeCode-Exception-Id': str(exc_id),
319 'RhodeCode-Exception-Id': str(exc_id),
321 'RhodeCode-Exception-Type': str(exc_type_name)
320 'RhodeCode-Exception-Type': str(exc_type_name)
322 }
321 }
323 err_resp = jsonrpc_error(
322 err_resp = jsonrpc_error(
324 request, retid=request.rpc_id, message='Internal server error',
323 request, retid=request.rpc_id, message='Internal server error',
325 headers=error_headers)
324 headers=error_headers)
326 if statsd:
325 if statsd:
327 statsd.incr('rhodecode_api_call_fail_total')
326 statsd.incr('rhodecode_api_call_fail_total')
328 return err_resp
327 return err_resp
329
328
330
329
331 def setup_request(request):
330 def setup_request(request):
332 """
331 """
333 Parse a JSON-RPC request body. It's used inside the predicates method
332 Parse a JSON-RPC request body. It's used inside the predicates method
334 to validate and bootstrap requests for usage in rpc calls.
333 to validate and bootstrap requests for usage in rpc calls.
335
334
336 We need to raise JSONRPCError here if we want to return some errors back to
335 We need to raise JSONRPCError here if we want to return some errors back to
337 user.
336 user.
338 """
337 """
339
338
340 log.debug('Executing setup request: %r', request)
339 log.debug('Executing setup request: %r', request)
341 request.rpc_ip_addr = get_ip_addr(request.environ)
340 request.rpc_ip_addr = get_ip_addr(request.environ)
342 # TODO(marcink): deprecate GET at some point
341 # TODO(marcink): deprecate GET at some point
343 if request.method not in ['POST', 'GET']:
342 if request.method not in ['POST', 'GET']:
344 log.debug('unsupported request method "%s"', request.method)
343 log.debug('unsupported request method "%s"', request.method)
345 raise JSONRPCError(
344 raise JSONRPCError(
346 'unsupported request method "%s". Please use POST' % request.method)
345 'unsupported request method "%s". Please use POST' % request.method)
347
346
348 if 'CONTENT_LENGTH' not in request.environ:
347 if 'CONTENT_LENGTH' not in request.environ:
349 log.debug("No Content-Length")
348 log.debug("No Content-Length")
350 raise JSONRPCError("Empty body, No Content-Length in request")
349 raise JSONRPCError("Empty body, No Content-Length in request")
351
350
352 else:
351 else:
353 length = request.environ['CONTENT_LENGTH']
352 length = request.environ['CONTENT_LENGTH']
354 log.debug('Content-Length: %s', length)
353 log.debug('Content-Length: %s', length)
355
354
356 if length == 0:
355 if length == 0:
357 log.debug("Content-Length is 0")
356 log.debug("Content-Length is 0")
358 raise JSONRPCError("Content-Length is 0")
357 raise JSONRPCError("Content-Length is 0")
359
358
360 raw_body = request.body
359 raw_body = request.body
361 log.debug("Loading JSON body now")
360 log.debug("Loading JSON body now")
362 try:
361 try:
363 json_body = ext_json.json.loads(raw_body)
362 json_body = ext_json.json.loads(raw_body)
364 except ValueError as e:
363 except ValueError as e:
365 # catch JSON errors Here
364 # catch JSON errors Here
366 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
365 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
367
366
368 request.rpc_id = json_body.get('id')
367 request.rpc_id = json_body.get('id')
369 request.rpc_method = json_body.get('method')
368 request.rpc_method = json_body.get('method')
370
369
371 # check required base parameters
370 # check required base parameters
372 try:
371 try:
373 api_key = json_body.get('api_key')
372 api_key = json_body.get('api_key')
374 if not api_key:
373 if not api_key:
375 api_key = json_body.get('auth_token')
374 api_key = json_body.get('auth_token')
376
375
377 if not api_key:
376 if not api_key:
378 raise KeyError('api_key or auth_token')
377 raise KeyError('api_key or auth_token')
379
378
380 # TODO(marcink): support passing in token in request header
379 # TODO(marcink): support passing in token in request header
381
380
382 request.rpc_api_key = api_key
381 request.rpc_api_key = api_key
383 request.rpc_id = json_body['id']
382 request.rpc_id = json_body['id']
384 request.rpc_method = json_body['method']
383 request.rpc_method = json_body['method']
385 request.rpc_params = json_body['args'] \
384 request.rpc_params = json_body['args'] \
386 if isinstance(json_body['args'], dict) else {}
385 if isinstance(json_body['args'], dict) else {}
387
386
388 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
387 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
389 except KeyError as e:
388 except KeyError as e:
390 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
389 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
391
390
392 log.debug('setup complete, now handling method:%s rpcid:%s',
391 log.debug('setup complete, now handling method:%s rpcid:%s',
393 request.rpc_method, request.rpc_id, )
392 request.rpc_method, request.rpc_id, )
394
393
395
394
396 class RoutePredicate(object):
395 class RoutePredicate(object):
397 def __init__(self, val, config):
396 def __init__(self, val, config):
398 self.val = val
397 self.val = val
399
398
400 def text(self):
399 def text(self):
401 return f'jsonrpc route = {self.val}'
400 return f'jsonrpc route = {self.val}'
402
401
403 phash = text
402 phash = text
404
403
405 def __call__(self, info, request):
404 def __call__(self, info, request):
406 if self.val:
405 if self.val:
407 # potentially setup and bootstrap our call
406 # potentially setup and bootstrap our call
408 setup_request(request)
407 setup_request(request)
409
408
410 # Always return True so that even if it isn't a valid RPC it
409 # Always return True so that even if it isn't a valid RPC it
411 # will fall through to the underlaying handlers like notfound_view
410 # will fall through to the underlaying handlers like notfound_view
412 return True
411 return True
413
412
414
413
415 class NotFoundPredicate(object):
414 class NotFoundPredicate(object):
416 def __init__(self, val, config):
415 def __init__(self, val, config):
417 self.val = val
416 self.val = val
418 self.methods = config.registry.jsonrpc_methods
417 self.methods = config.registry.jsonrpc_methods
419
418
420 def text(self):
419 def text(self):
421 return f'jsonrpc method not found = {self.val}'
420 return f'jsonrpc method not found = {self.val}'
422
421
423 phash = text
422 phash = text
424
423
425 def __call__(self, info, request):
424 def __call__(self, info, request):
426 return hasattr(request, 'rpc_method')
425 return hasattr(request, 'rpc_method')
427
426
428
427
429 class MethodPredicate(object):
428 class MethodPredicate(object):
430 def __init__(self, val, config):
429 def __init__(self, val, config):
431 self.method = val
430 self.method = val
432
431
433 def text(self):
432 def text(self):
434 return f'jsonrpc method = {self.method}'
433 return f'jsonrpc method = {self.method}'
435
434
436 phash = text
435 phash = text
437
436
438 def __call__(self, context, request):
437 def __call__(self, context, request):
439 # we need to explicitly return False here, so pyramid doesn't try to
438 # we need to explicitly return False here, so pyramid doesn't try to
440 # execute our view directly. We need our main handler to execute things
439 # execute our view directly. We need our main handler to execute things
441 return getattr(request, 'rpc_method') == self.method
440 return getattr(request, 'rpc_method') == self.method
442
441
443
442
444 def add_jsonrpc_method(config, view, **kwargs):
443 def add_jsonrpc_method(config, view, **kwargs):
445 # pop the method name
444 # pop the method name
446 method = kwargs.pop('method', None)
445 method = kwargs.pop('method', None)
447
446
448 if method is None:
447 if method is None:
449 raise ConfigurationError(
448 raise ConfigurationError(
450 'Cannot register a JSON-RPC method without specifying the "method"')
449 'Cannot register a JSON-RPC method without specifying the "method"')
451
450
452 # we define custom predicate, to enable to detect conflicting methods,
451 # we define custom predicate, to enable to detect conflicting methods,
453 # those predicates are kind of "translation" from the decorator variables
452 # those predicates are kind of "translation" from the decorator variables
454 # to internal predicates names
453 # to internal predicates names
455
454
456 kwargs['jsonrpc_method'] = method
455 kwargs['jsonrpc_method'] = method
457
456
458 # register our view into global view store for validation
457 # register our view into global view store for validation
459 config.registry.jsonrpc_methods[method] = view
458 config.registry.jsonrpc_methods[method] = view
460
459
461 # we're using our main request_view handler, here, so each method
460 # we're using our main request_view handler, here, so each method
462 # has a unified handler for itself
461 # has a unified handler for itself
463 config.add_view(request_view, route_name='apiv2', **kwargs)
462 config.add_view(request_view, route_name='apiv2', **kwargs)
464
463
465
464
466 class jsonrpc_method(object):
465 class jsonrpc_method(object):
467 """
466 """
468 decorator that works similar to @add_view_config decorator,
467 decorator that works similar to @add_view_config decorator,
469 but tailored for our JSON RPC
468 but tailored for our JSON RPC
470 """
469 """
471
470
472 venusian = venusian # for testing injection
471 venusian = venusian # for testing injection
473
472
474 def __init__(self, method=None, **kwargs):
473 def __init__(self, method=None, **kwargs):
475 self.method = method
474 self.method = method
476 self.kwargs = kwargs
475 self.kwargs = kwargs
477
476
478 def __call__(self, wrapped):
477 def __call__(self, wrapped):
479 kwargs = self.kwargs.copy()
478 kwargs = self.kwargs.copy()
480 kwargs['method'] = self.method or wrapped.__name__
479 kwargs['method'] = self.method or wrapped.__name__
481 depth = kwargs.pop('_depth', 0)
480 depth = kwargs.pop('_depth', 0)
482
481
483 def callback(context, name, ob):
482 def callback(context, name, ob):
484 config = context.config.with_package(info.module)
483 config = context.config.with_package(info.module)
485 config.add_jsonrpc_method(view=ob, **kwargs)
484 config.add_jsonrpc_method(view=ob, **kwargs)
486
485
487 info = venusian.attach(wrapped, callback, category='pyramid',
486 info = venusian.attach(wrapped, callback, category='pyramid',
488 depth=depth + 1)
487 depth=depth + 1)
489 if info.scope == 'class':
488 if info.scope == 'class':
490 # ensure that attr is set if decorating a class method
489 # ensure that attr is set if decorating a class method
491 kwargs.setdefault('attr', wrapped.__name__)
490 kwargs.setdefault('attr', wrapped.__name__)
492
491
493 kwargs['_info'] = info.codeinfo # fbo action_method
492 kwargs['_info'] = info.codeinfo # fbo action_method
494 return wrapped
493 return wrapped
495
494
496
495
497 class jsonrpc_deprecated_method(object):
496 class jsonrpc_deprecated_method(object):
498 """
497 """
499 Marks method as deprecated, adds log.warning, and inject special key to
498 Marks method as deprecated, adds log.warning, and inject special key to
500 the request variable to mark method as deprecated.
499 the request variable to mark method as deprecated.
501 Also injects special docstring that extract_docs will catch to mark
500 Also injects special docstring that extract_docs will catch to mark
502 method as deprecated.
501 method as deprecated.
503
502
504 :param use_method: specify which method should be used instead of
503 :param use_method: specify which method should be used instead of
505 the decorated one
504 the decorated one
506
505
507 Use like::
506 Use like::
508
507
509 @jsonrpc_method()
508 @jsonrpc_method()
510 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
509 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
511 def old_func(request, apiuser, arg1, arg2):
510 def old_func(request, apiuser, arg1, arg2):
512 ...
511 ...
513 """
512 """
514
513
515 def __init__(self, use_method, deprecated_at_version):
514 def __init__(self, use_method, deprecated_at_version):
516 self.use_method = use_method
515 self.use_method = use_method
517 self.deprecated_at_version = deprecated_at_version
516 self.deprecated_at_version = deprecated_at_version
518 self.deprecated_msg = ''
517 self.deprecated_msg = ''
519
518
520 def __call__(self, func):
519 def __call__(self, func):
521 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
520 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
522 method=self.use_method)
521 method=self.use_method)
523
522
524 docstring = """\n
523 docstring = """\n
525 .. deprecated:: {version}
524 .. deprecated:: {version}
526
525
527 {deprecation_message}
526 {deprecation_message}
528
527
529 {original_docstring}
528 {original_docstring}
530 """
529 """
531 func.__doc__ = docstring.format(
530 func.__doc__ = docstring.format(
532 version=self.deprecated_at_version,
531 version=self.deprecated_at_version,
533 deprecation_message=self.deprecated_msg,
532 deprecation_message=self.deprecated_msg,
534 original_docstring=func.__doc__)
533 original_docstring=func.__doc__)
535 return decorator.decorator(self.__wrapper, func)
534 return decorator.decorator(self.__wrapper, func)
536
535
537 def __wrapper(self, func, *fargs, **fkwargs):
536 def __wrapper(self, func, *fargs, **fkwargs):
538 log.warning('DEPRECATED API CALL on function %s, please '
537 log.warning('DEPRECATED API CALL on function %s, please '
539 'use `%s` instead', func, self.use_method)
538 'use `%s` instead', func, self.use_method)
540 # alter function docstring to mark as deprecated, this is picked up
539 # alter function docstring to mark as deprecated, this is picked up
541 # via fabric file that generates API DOC.
540 # via fabric file that generates API DOC.
542 result = func(*fargs, **fkwargs)
541 result = func(*fargs, **fkwargs)
543
542
544 request = fargs[0]
543 request = fargs[0]
545 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
544 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
546 return result
545 return result
547
546
548
547
549 def add_api_methods(config):
548 def add_api_methods(config):
550 from rhodecode.api.views import (
549 from rhodecode.api.views import (
551 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
550 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
552 server_api, search_api, testing_api, user_api, user_group_api)
551 server_api, search_api, testing_api, user_api, user_group_api)
553
552
554 config.scan('rhodecode.api.views')
553 config.scan('rhodecode.api.views')
555
554
556
555
557 def includeme(config):
556 def includeme(config):
558 plugin_module = 'rhodecode.api'
557 plugin_module = 'rhodecode.api'
559 plugin_settings = get_plugin_settings(
558 plugin_settings = get_plugin_settings(
560 plugin_module, config.registry.settings)
559 plugin_module, config.registry.settings)
561
560
562 if not hasattr(config.registry, 'jsonrpc_methods'):
561 if not hasattr(config.registry, 'jsonrpc_methods'):
563 config.registry.jsonrpc_methods = OrderedDict()
562 config.registry.jsonrpc_methods = OrderedDict()
564
563
565 # match filter by given method only
564 # match filter by given method only
566 config.add_view_predicate('jsonrpc_method', MethodPredicate)
565 config.add_view_predicate('jsonrpc_method', MethodPredicate)
567 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
566 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
568
567
569 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
568 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
570 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
569 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
571
570
572 config.add_route_predicate(
571 config.add_route_predicate(
573 'jsonrpc_call', RoutePredicate)
572 'jsonrpc_call', RoutePredicate)
574
573
575 config.add_route(
574 config.add_route(
576 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
575 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
577
576
578 # register some exception handling view
577 # register some exception handling view
579 config.add_view(exception_view, context=JSONRPCBaseError)
578 config.add_view(exception_view, context=JSONRPCBaseError)
580 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
579 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
581
580
582 add_api_methods(config)
581 add_api_methods(config)
@@ -1,637 +1,637 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 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 os
19 import os
20 import sys
20 import sys
21 import collections
21 import collections
22 import tempfile
22 import tempfile
23 import time
23 import time
24 import logging.config
24 import logging.config
25
25
26 from paste.gzipper import make_gzip_middleware
26 from paste.gzipper import make_gzip_middleware
27 import pyramid.events
27 import pyramid.events
28 from pyramid.wsgi import wsgiapp
28 from pyramid.wsgi import wsgiapp
29 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.config import Configurator
29 from pyramid.config import Configurator
31 from pyramid.settings import asbool, aslist
30 from pyramid.settings import asbool, aslist
32 from pyramid.httpexceptions import (
31 from pyramid.httpexceptions import (
33 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
32 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
34 from pyramid.renderers import render_to_response
33 from pyramid.renderers import render_to_response
35
34
35 from rhodecode import api
36 from rhodecode.model import meta
36 from rhodecode.model import meta
37 from rhodecode.config import patches
37 from rhodecode.config import patches
38 from rhodecode.config import utils as config_utils
38 from rhodecode.config import utils as config_utils
39 from rhodecode.config.settings_maker import SettingsMaker
39 from rhodecode.config.settings_maker import SettingsMaker
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 import rhodecode.events
43 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 from rhodecode.lib.middleware.vcs import VCSMiddleware
44 from rhodecode.lib.request import Request
44 from rhodecode.lib.request import Request
45 from rhodecode.lib.vcs import VCSCommunicationError
45 from rhodecode.lib.vcs import VCSCommunicationError
46 from rhodecode.lib.exceptions import VCSServerUnavailable
46 from rhodecode.lib.exceptions import VCSServerUnavailable
47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 from rhodecode.lib.utils2 import AttributeDict
50 from rhodecode.lib.utils2 import AttributeDict
51 from rhodecode.lib.exc_tracking import store_exception, format_exc
51 from rhodecode.lib.exc_tracking import store_exception, format_exc
52 from rhodecode.subscribers import (
52 from rhodecode.subscribers import (
53 scan_repositories_if_enabled, write_js_routes_if_enabled,
53 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 write_metadata_if_needed, write_usage_data)
54 write_metadata_if_needed, write_usage_data)
55 from rhodecode.lib.statsd_client import StatsdClient
55 from rhodecode.lib.statsd_client import StatsdClient
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 def is_http_error(response):
60 def is_http_error(response):
61 # error which should have traceback
61 # error which should have traceback
62 return response.status_code > 499
62 return response.status_code > 499
63
63
64
64
65 def should_load_all():
65 def should_load_all():
66 """
66 """
67 Returns if all application components should be loaded. In some cases it's
67 Returns if all application components should be loaded. In some cases it's
68 desired to skip apps loading for faster shell script execution
68 desired to skip apps loading for faster shell script execution
69 """
69 """
70 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
70 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
71 if ssh_cmd:
71 if ssh_cmd:
72 return False
72 return False
73
73
74 return True
74 return True
75
75
76
76
77 def make_pyramid_app(global_config, **settings):
77 def make_pyramid_app(global_config, **settings):
78 """
78 """
79 Constructs the WSGI application based on Pyramid.
79 Constructs the WSGI application based on Pyramid.
80
80
81 Specials:
81 Specials:
82
82
83 * The application can also be integrated like a plugin via the call to
83 * The application can also be integrated like a plugin via the call to
84 `includeme`. This is accompanied with the other utility functions which
84 `includeme`. This is accompanied with the other utility functions which
85 are called. Changing this should be done with great care to not break
85 are called. Changing this should be done with great care to not break
86 cases when these fragments are assembled from another place.
86 cases when these fragments are assembled from another place.
87
87
88 """
88 """
89 start_time = time.time()
89 start_time = time.time()
90 log.info('Pyramid app config starting')
90 log.info('Pyramid app config starting')
91
91
92 sanitize_settings_and_apply_defaults(global_config, settings)
92 sanitize_settings_and_apply_defaults(global_config, settings)
93
93
94 # init and bootstrap StatsdClient
94 # init and bootstrap StatsdClient
95 StatsdClient.setup(settings)
95 StatsdClient.setup(settings)
96
96
97 config = Configurator(settings=settings)
97 config = Configurator(settings=settings)
98 # Init our statsd at very start
98 # Init our statsd at very start
99 config.registry.statsd = StatsdClient.statsd
99 config.registry.statsd = StatsdClient.statsd
100
100
101 # Apply compatibility patches
101 # Apply compatibility patches
102 patches.inspect_getargspec()
102 patches.inspect_getargspec()
103
103
104 load_pyramid_environment(global_config, settings)
104 load_pyramid_environment(global_config, settings)
105
105
106 # Static file view comes first
106 # Static file view comes first
107 includeme_first(config)
107 includeme_first(config)
108
108
109 includeme(config)
109 includeme(config)
110
110
111 pyramid_app = config.make_wsgi_app()
111 pyramid_app = config.make_wsgi_app()
112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
113 pyramid_app.config = config
113 pyramid_app.config = config
114
114
115 celery_settings = get_celery_config(settings)
115 celery_settings = get_celery_config(settings)
116 config.configure_celery(celery_settings)
116 config.configure_celery(celery_settings)
117
117
118 # creating the app uses a connection - return it after we are done
118 # creating the app uses a connection - return it after we are done
119 meta.Session.remove()
119 meta.Session.remove()
120
120
121 total_time = time.time() - start_time
121 total_time = time.time() - start_time
122 log.info('Pyramid app created and configured in %.2fs', total_time)
122 log.info('Pyramid app created and configured in %.2fs', total_time)
123 return pyramid_app
123 return pyramid_app
124
124
125
125
126 def get_celery_config(settings):
126 def get_celery_config(settings):
127 """
127 """
128 Converts basic ini configuration into celery 4.X options
128 Converts basic ini configuration into celery 4.X options
129 """
129 """
130
130
131 def key_converter(key_name):
131 def key_converter(key_name):
132 pref = 'celery.'
132 pref = 'celery.'
133 if key_name.startswith(pref):
133 if key_name.startswith(pref):
134 return key_name[len(pref):].replace('.', '_').lower()
134 return key_name[len(pref):].replace('.', '_').lower()
135
135
136 def type_converter(parsed_key, value):
136 def type_converter(parsed_key, value):
137 # cast to int
137 # cast to int
138 if value.isdigit():
138 if value.isdigit():
139 return int(value)
139 return int(value)
140
140
141 # cast to bool
141 # cast to bool
142 if value.lower() in ['true', 'false', 'True', 'False']:
142 if value.lower() in ['true', 'false', 'True', 'False']:
143 return value.lower() == 'true'
143 return value.lower() == 'true'
144 return value
144 return value
145
145
146 celery_config = {}
146 celery_config = {}
147 for k, v in settings.items():
147 for k, v in settings.items():
148 pref = 'celery.'
148 pref = 'celery.'
149 if k.startswith(pref):
149 if k.startswith(pref):
150 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
150 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
151
151
152 # TODO:rethink if we want to support celerybeat based file config, probably NOT
152 # TODO:rethink if we want to support celerybeat based file config, probably NOT
153 # beat_config = {}
153 # beat_config = {}
154 # for section in parser.sections():
154 # for section in parser.sections():
155 # if section.startswith('celerybeat:'):
155 # if section.startswith('celerybeat:'):
156 # name = section.split(':', 1)[1]
156 # name = section.split(':', 1)[1]
157 # beat_config[name] = get_beat_config(parser, section)
157 # beat_config[name] = get_beat_config(parser, section)
158
158
159 # final compose of settings
159 # final compose of settings
160 celery_settings = {}
160 celery_settings = {}
161
161
162 if celery_config:
162 if celery_config:
163 celery_settings.update(celery_config)
163 celery_settings.update(celery_config)
164 # if beat_config:
164 # if beat_config:
165 # celery_settings.update({'beat_schedule': beat_config})
165 # celery_settings.update({'beat_schedule': beat_config})
166
166
167 return celery_settings
167 return celery_settings
168
168
169
169
170 def not_found_view(request):
170 def not_found_view(request):
171 """
171 """
172 This creates the view which should be registered as not-found-view to
172 This creates the view which should be registered as not-found-view to
173 pyramid.
173 pyramid.
174 """
174 """
175
175
176 if not getattr(request, 'vcs_call', None):
176 if not getattr(request, 'vcs_call', None):
177 # handle like regular case with our error_handler
177 # handle like regular case with our error_handler
178 return error_handler(HTTPNotFound(), request)
178 return error_handler(HTTPNotFound(), request)
179
179
180 # handle not found view as a vcs call
180 # handle not found view as a vcs call
181 settings = request.registry.settings
181 settings = request.registry.settings
182 ae_client = getattr(request, 'ae_client', None)
182 ae_client = getattr(request, 'ae_client', None)
183 vcs_app = VCSMiddleware(
183 vcs_app = VCSMiddleware(
184 HTTPNotFound(), request.registry, settings,
184 HTTPNotFound(), request.registry, settings,
185 appenlight_client=ae_client)
185 appenlight_client=ae_client)
186
186
187 return wsgiapp(vcs_app)(None, request)
187 return wsgiapp(vcs_app)(None, request)
188
188
189
189
190 def error_handler(exception, request):
190 def error_handler(exception, request):
191 import rhodecode
191 import rhodecode
192 from rhodecode.lib import helpers
192 from rhodecode.lib import helpers
193
193
194 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
194 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
195
195
196 base_response = HTTPInternalServerError()
196 base_response = HTTPInternalServerError()
197 # prefer original exception for the response since it may have headers set
197 # prefer original exception for the response since it may have headers set
198 if isinstance(exception, HTTPException):
198 if isinstance(exception, HTTPException):
199 base_response = exception
199 base_response = exception
200 elif isinstance(exception, VCSCommunicationError):
200 elif isinstance(exception, VCSCommunicationError):
201 base_response = VCSServerUnavailable()
201 base_response = VCSServerUnavailable()
202
202
203 if is_http_error(base_response):
203 if is_http_error(base_response):
204 traceback_info = format_exc(request.exc_info)
204 traceback_info = format_exc(request.exc_info)
205 log.error(
205 log.error(
206 'error occurred handling this request for path: %s, \n%s',
206 'error occurred handling this request for path: %s, \n%s',
207 request.path, traceback_info)
207 request.path, traceback_info)
208
208
209 error_explanation = base_response.explanation or str(base_response)
209 error_explanation = base_response.explanation or str(base_response)
210 if base_response.status_code == 404:
210 if base_response.status_code == 404:
211 error_explanation += " Optionally you don't have permission to access this page."
211 error_explanation += " Optionally you don't have permission to access this page."
212 c = AttributeDict()
212 c = AttributeDict()
213 c.error_message = base_response.status
213 c.error_message = base_response.status
214 c.error_explanation = error_explanation
214 c.error_explanation = error_explanation
215 c.visual = AttributeDict()
215 c.visual = AttributeDict()
216
216
217 c.visual.rhodecode_support_url = (
217 c.visual.rhodecode_support_url = (
218 request.registry.settings.get('rhodecode_support_url') or
218 request.registry.settings.get('rhodecode_support_url') or
219 request.route_url('rhodecode_support')
219 request.route_url('rhodecode_support')
220 )
220 )
221 c.redirect_time = 0
221 c.redirect_time = 0
222 c.rhodecode_name = rhodecode_title
222 c.rhodecode_name = rhodecode_title
223 if not c.rhodecode_name:
223 if not c.rhodecode_name:
224 c.rhodecode_name = 'Rhodecode'
224 c.rhodecode_name = 'Rhodecode'
225
225
226 c.causes = []
226 c.causes = []
227 if is_http_error(base_response):
227 if is_http_error(base_response):
228 c.causes.append('Server is overloaded.')
228 c.causes.append('Server is overloaded.')
229 c.causes.append('Server database connection is lost.')
229 c.causes.append('Server database connection is lost.')
230 c.causes.append('Server expected unhandled error.')
230 c.causes.append('Server expected unhandled error.')
231
231
232 if hasattr(base_response, 'causes'):
232 if hasattr(base_response, 'causes'):
233 c.causes = base_response.causes
233 c.causes = base_response.causes
234
234
235 c.messages = helpers.flash.pop_messages(request=request)
235 c.messages = helpers.flash.pop_messages(request=request)
236 exc_info = sys.exc_info()
236 exc_info = sys.exc_info()
237 c.exception_id = id(exc_info)
237 c.exception_id = id(exc_info)
238 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
238 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
239 or base_response.status_code > 499
239 or base_response.status_code > 499
240 c.exception_id_url = request.route_url(
240 c.exception_id_url = request.route_url(
241 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
241 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
242
242
243 debug_mode = rhodecode.ConfigGet().get_bool('debug')
243 debug_mode = rhodecode.ConfigGet().get_bool('debug')
244 if c.show_exception_id:
244 if c.show_exception_id:
245 store_exception(c.exception_id, exc_info)
245 store_exception(c.exception_id, exc_info)
246 c.exception_debug = debug_mode
246 c.exception_debug = debug_mode
247 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
247 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
248
248
249 if debug_mode:
249 if debug_mode:
250 try:
250 try:
251 from rich.traceback import install
251 from rich.traceback import install
252 install(show_locals=True)
252 install(show_locals=True)
253 log.debug('Installing rich tracebacks...')
253 log.debug('Installing rich tracebacks...')
254 except ImportError:
254 except ImportError:
255 pass
255 pass
256
256
257 response = render_to_response(
257 response = render_to_response(
258 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
258 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
259 response=base_response)
259 response=base_response)
260
260
261 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
261 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
262
262
263 statsd = request.registry.statsd
263 statsd = request.registry.statsd
264 if statsd and base_response.status_code > 499:
264 if statsd and base_response.status_code > 499:
265 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
265 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
266 statsd.incr('rhodecode_exception_total',
266 statsd.incr('rhodecode_exception_total',
267 tags=["exc_source:web",
267 tags=["exc_source:web",
268 f"http_code:{base_response.status_code}",
268 f"http_code:{base_response.status_code}",
269 f"type:{exc_type}"])
269 f"type:{exc_type}"])
270
270
271 return response
271 return response
272
272
273
273
274 def includeme_first(config):
274 def includeme_first(config):
275 # redirect automatic browser favicon.ico requests to correct place
275 # redirect automatic browser favicon.ico requests to correct place
276 def favicon_redirect(context, request):
276 def favicon_redirect(context, request):
277 return HTTPFound(
277 return HTTPFound(
278 request.static_path('rhodecode:public/images/favicon.ico'))
278 request.static_path('rhodecode:public/images/favicon.ico'))
279
279
280 config.add_view(favicon_redirect, route_name='favicon')
280 config.add_view(favicon_redirect, route_name='favicon')
281 config.add_route('favicon', '/favicon.ico')
281 config.add_route('favicon', '/favicon.ico')
282
282
283 def robots_redirect(context, request):
283 def robots_redirect(context, request):
284 return HTTPFound(
284 return HTTPFound(
285 request.static_path('rhodecode:public/robots.txt'))
285 request.static_path('rhodecode:public/robots.txt'))
286
286
287 config.add_view(robots_redirect, route_name='robots')
287 config.add_view(robots_redirect, route_name='robots')
288 config.add_route('robots', '/robots.txt')
288 config.add_route('robots', '/robots.txt')
289
289
290 config.add_static_view(
290 config.add_static_view(
291 '_static/deform', 'deform:static')
291 '_static/deform', 'deform:static')
292 config.add_static_view(
292 config.add_static_view(
293 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
293 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
294
294
295
295
296 ce_auth_resources = [
296 ce_auth_resources = [
297 'rhodecode.authentication.plugins.auth_crowd',
297 'rhodecode.authentication.plugins.auth_crowd',
298 'rhodecode.authentication.plugins.auth_headers',
298 'rhodecode.authentication.plugins.auth_headers',
299 'rhodecode.authentication.plugins.auth_jasig_cas',
299 'rhodecode.authentication.plugins.auth_jasig_cas',
300 'rhodecode.authentication.plugins.auth_ldap',
300 'rhodecode.authentication.plugins.auth_ldap',
301 'rhodecode.authentication.plugins.auth_pam',
301 'rhodecode.authentication.plugins.auth_pam',
302 'rhodecode.authentication.plugins.auth_rhodecode',
302 'rhodecode.authentication.plugins.auth_rhodecode',
303 'rhodecode.authentication.plugins.auth_token',
303 'rhodecode.authentication.plugins.auth_token',
304 ]
304 ]
305
305
306
306
307 def includeme(config, auth_resources=None):
307 def includeme(config, auth_resources=None):
308 from rhodecode.lib.celerylib.loader import configure_celery
308 from rhodecode.lib.celerylib.loader import configure_celery
309 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
309 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
310 settings = config.registry.settings
310 settings = config.registry.settings
311 config.set_request_factory(Request)
311 config.set_request_factory(Request)
312
312
313 # plugin information
313 # plugin information
314 config.registry.rhodecode_plugins = collections.OrderedDict()
314 config.registry.rhodecode_plugins = collections.OrderedDict()
315
315
316 config.add_directive(
316 config.add_directive(
317 'register_rhodecode_plugin', register_rhodecode_plugin)
317 'register_rhodecode_plugin', register_rhodecode_plugin)
318
318
319 config.add_directive('configure_celery', configure_celery)
319 config.add_directive('configure_celery', configure_celery)
320
320
321 if settings.get('appenlight', False):
321 if settings.get('appenlight', False):
322 config.include('appenlight_client.ext.pyramid_tween')
322 config.include('appenlight_client.ext.pyramid_tween')
323
323
324 load_all = should_load_all()
324 load_all = should_load_all()
325
325
326 # Includes which are required. The application would fail without them.
326 # Includes which are required. The application would fail without them.
327 config.include('pyramid_mako')
327 config.include('pyramid_mako')
328 config.include('rhodecode.lib.rc_beaker')
328 config.include('rhodecode.lib.rc_beaker')
329 config.include('rhodecode.lib.rc_cache')
329 config.include('rhodecode.lib.rc_cache')
330 config.include('rhodecode.lib.rc_cache.archive_cache')
330 config.include('rhodecode.lib.rc_cache.archive_cache')
331
331
332 config.include('rhodecode.apps._base.navigation')
332 config.include('rhodecode.apps._base.navigation')
333 config.include('rhodecode.apps._base.subscribers')
333 config.include('rhodecode.apps._base.subscribers')
334 config.include('rhodecode.tweens')
334 config.include('rhodecode.tweens')
335 config.include('rhodecode.authentication')
335 config.include('rhodecode.authentication')
336
336
337 if load_all:
337 if load_all:
338
338
339 # load CE authentication plugins
339 # load CE authentication plugins
340
340
341 if auth_resources:
341 if auth_resources:
342 ce_auth_resources.extend(auth_resources)
342 ce_auth_resources.extend(auth_resources)
343
343
344 for resource in ce_auth_resources:
344 for resource in ce_auth_resources:
345 config.include(resource)
345 config.include(resource)
346
346
347 # Auto discover authentication plugins and include their configuration.
347 # Auto discover authentication plugins and include their configuration.
348 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
348 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
349 from rhodecode.authentication import discover_legacy_plugins
349 from rhodecode.authentication import discover_legacy_plugins
350 discover_legacy_plugins(config)
350 discover_legacy_plugins(config)
351
351
352 # apps
352 # apps
353 if load_all:
353 if load_all:
354 log.debug('Starting config.include() calls')
354 log.debug('Starting config.include() calls')
355 config.include('rhodecode.api.includeme')
355 config.include('rhodecode.api.includeme')
356 config.include('rhodecode.apps._base.includeme')
356 config.include('rhodecode.apps._base.includeme')
357 config.include('rhodecode.apps._base.navigation.includeme')
357 config.include('rhodecode.apps._base.navigation.includeme')
358 config.include('rhodecode.apps._base.subscribers.includeme')
358 config.include('rhodecode.apps._base.subscribers.includeme')
359 config.include('rhodecode.apps.hovercards.includeme')
359 config.include('rhodecode.apps.hovercards.includeme')
360 config.include('rhodecode.apps.ops.includeme')
360 config.include('rhodecode.apps.ops.includeme')
361 config.include('rhodecode.apps.channelstream.includeme')
361 config.include('rhodecode.apps.channelstream.includeme')
362 config.include('rhodecode.apps.file_store.includeme')
362 config.include('rhodecode.apps.file_store.includeme')
363 config.include('rhodecode.apps.admin.includeme')
363 config.include('rhodecode.apps.admin.includeme')
364 config.include('rhodecode.apps.login.includeme')
364 config.include('rhodecode.apps.login.includeme')
365 config.include('rhodecode.apps.home.includeme')
365 config.include('rhodecode.apps.home.includeme')
366 config.include('rhodecode.apps.journal.includeme')
366 config.include('rhodecode.apps.journal.includeme')
367
367
368 config.include('rhodecode.apps.repository.includeme')
368 config.include('rhodecode.apps.repository.includeme')
369 config.include('rhodecode.apps.repo_group.includeme')
369 config.include('rhodecode.apps.repo_group.includeme')
370 config.include('rhodecode.apps.user_group.includeme')
370 config.include('rhodecode.apps.user_group.includeme')
371 config.include('rhodecode.apps.search.includeme')
371 config.include('rhodecode.apps.search.includeme')
372 config.include('rhodecode.apps.user_profile.includeme')
372 config.include('rhodecode.apps.user_profile.includeme')
373 config.include('rhodecode.apps.user_group_profile.includeme')
373 config.include('rhodecode.apps.user_group_profile.includeme')
374 config.include('rhodecode.apps.my_account.includeme')
374 config.include('rhodecode.apps.my_account.includeme')
375 config.include('rhodecode.apps.gist.includeme')
375 config.include('rhodecode.apps.gist.includeme')
376
376
377 config.include('rhodecode.apps.svn_support.includeme')
377 config.include('rhodecode.apps.svn_support.includeme')
378 config.include('rhodecode.apps.ssh_support.includeme')
378 config.include('rhodecode.apps.ssh_support.includeme')
379 config.include('rhodecode.apps.debug_style')
379 config.include('rhodecode.apps.debug_style')
380
380
381 if load_all:
381 if load_all:
382 config.include('rhodecode.integrations.includeme')
382 config.include('rhodecode.integrations.includeme')
383 config.include('rhodecode.integrations.routes.includeme')
383 config.include('rhodecode.integrations.routes.includeme')
384
384
385 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
385 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
386 settings['default_locale_name'] = settings.get('lang', 'en')
386 settings['default_locale_name'] = settings.get('lang', 'en')
387 config.add_translation_dirs('rhodecode:i18n/')
387 config.add_translation_dirs('rhodecode:i18n/')
388
388
389 # Add subscribers.
389 # Add subscribers.
390 if load_all:
390 if load_all:
391 log.debug('Adding subscribers...')
391 log.debug('Adding subscribers...')
392 config.add_subscriber(scan_repositories_if_enabled,
392 config.add_subscriber(scan_repositories_if_enabled,
393 pyramid.events.ApplicationCreated)
393 pyramid.events.ApplicationCreated)
394 config.add_subscriber(write_metadata_if_needed,
394 config.add_subscriber(write_metadata_if_needed,
395 pyramid.events.ApplicationCreated)
395 pyramid.events.ApplicationCreated)
396 config.add_subscriber(write_usage_data,
396 config.add_subscriber(write_usage_data,
397 pyramid.events.ApplicationCreated)
397 pyramid.events.ApplicationCreated)
398 config.add_subscriber(write_js_routes_if_enabled,
398 config.add_subscriber(write_js_routes_if_enabled,
399 pyramid.events.ApplicationCreated)
399 pyramid.events.ApplicationCreated)
400
400
401
401
402 # Set the default renderer for HTML templates to mako.
402 # Set the default renderer for HTML templates to mako.
403 config.add_mako_renderer('.html')
403 config.add_mako_renderer('.html')
404
404
405 config.add_renderer(
405 config.add_renderer(
406 name='json_ext',
406 name='json_ext',
407 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
407 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
408
408
409 config.add_renderer(
409 config.add_renderer(
410 name='string_html',
410 name='string_html',
411 factory='rhodecode.lib.string_renderer.html')
411 factory='rhodecode.lib.string_renderer.html')
412
412
413 # include RhodeCode plugins
413 # include RhodeCode plugins
414 includes = aslist(settings.get('rhodecode.includes', []))
414 includes = aslist(settings.get('rhodecode.includes', []))
415 log.debug('processing rhodecode.includes data...')
415 log.debug('processing rhodecode.includes data...')
416 for inc in includes:
416 for inc in includes:
417 config.include(inc)
417 config.include(inc)
418
418
419 # custom not found view, if our pyramid app doesn't know how to handle
419 # custom not found view, if our pyramid app doesn't know how to handle
420 # the request pass it to potential VCS handling ap
420 # the request pass it to potential VCS handling ap
421 config.add_notfound_view(not_found_view)
421 config.add_notfound_view(not_found_view)
422 if not settings.get('debugtoolbar.enabled', False):
422 if not settings.get('debugtoolbar.enabled', False):
423 # disabled debugtoolbar handle all exceptions via the error_handlers
423 # disabled debugtoolbar handle all exceptions via the error_handlers
424 config.add_view(error_handler, context=Exception)
424 config.add_view(error_handler, context=Exception)
425
425
426 # all errors including 403/404/50X
426 # all errors including 403/404/50X
427 config.add_view(error_handler, context=HTTPError)
427 config.add_view(error_handler, context=HTTPError)
428
428
429
429
430 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
430 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
431 """
431 """
432 Apply outer WSGI middlewares around the application.
432 Apply outer WSGI middlewares around the application.
433 """
433 """
434 registry = config.registry
434 registry = config.registry
435 settings = registry.settings
435 settings = registry.settings
436
436
437 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
437 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
438 pyramid_app = HttpsFixup(pyramid_app, settings)
438 pyramid_app = HttpsFixup(pyramid_app, settings)
439
439
440 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
440 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
441 pyramid_app, settings)
441 pyramid_app, settings)
442 registry.ae_client = _ae_client
442 registry.ae_client = _ae_client
443
443
444 if settings['gzip_responses']:
444 if settings['gzip_responses']:
445 pyramid_app = make_gzip_middleware(
445 pyramid_app = make_gzip_middleware(
446 pyramid_app, settings, compress_level=1)
446 pyramid_app, settings, compress_level=1)
447
447
448 # this should be the outer most middleware in the wsgi stack since
448 # this should be the outer most middleware in the wsgi stack since
449 # middleware like Routes make database calls
449 # middleware like Routes make database calls
450 def pyramid_app_with_cleanup(environ, start_response):
450 def pyramid_app_with_cleanup(environ, start_response):
451 start = time.time()
451 start = time.time()
452 try:
452 try:
453 return pyramid_app(environ, start_response)
453 return pyramid_app(environ, start_response)
454 finally:
454 finally:
455 # Dispose current database session and rollback uncommitted
455 # Dispose current database session and rollback uncommitted
456 # transactions.
456 # transactions.
457 meta.Session.remove()
457 meta.Session.remove()
458
458
459 # In a single threaded mode server, on non sqlite db we should have
459 # In a single threaded mode server, on non sqlite db we should have
460 # '0 Current Checked out connections' at the end of a request,
460 # '0 Current Checked out connections' at the end of a request,
461 # if not, then something, somewhere is leaving a connection open
461 # if not, then something, somewhere is leaving a connection open
462 pool = meta.get_engine().pool
462 pool = meta.get_engine().pool
463 log.debug('sa pool status: %s', pool.status())
463 log.debug('sa pool status: %s', pool.status())
464 total = time.time() - start
464 total = time.time() - start
465 log.debug('Request processing finalized: %.4fs', total)
465 log.debug('Request processing finalized: %.4fs', total)
466
466
467 return pyramid_app_with_cleanup
467 return pyramid_app_with_cleanup
468
468
469
469
470 def sanitize_settings_and_apply_defaults(global_config, settings):
470 def sanitize_settings_and_apply_defaults(global_config, settings):
471 """
471 """
472 Applies settings defaults and does all type conversion.
472 Applies settings defaults and does all type conversion.
473
473
474 We would move all settings parsing and preparation into this place, so that
474 We would move all settings parsing and preparation into this place, so that
475 we have only one place left which deals with this part. The remaining parts
475 we have only one place left which deals with this part. The remaining parts
476 of the application would start to rely fully on well prepared settings.
476 of the application would start to rely fully on well prepared settings.
477
477
478 This piece would later be split up per topic to avoid a big fat monster
478 This piece would later be split up per topic to avoid a big fat monster
479 function.
479 function.
480 """
480 """
481 jn = os.path.join
481 jn = os.path.join
482
482
483 global_settings_maker = SettingsMaker(global_config)
483 global_settings_maker = SettingsMaker(global_config)
484 global_settings_maker.make_setting('debug', default=False, parser='bool')
484 global_settings_maker.make_setting('debug', default=False, parser='bool')
485 debug_enabled = asbool(global_config.get('debug'))
485 debug_enabled = asbool(global_config.get('debug'))
486
486
487 settings_maker = SettingsMaker(settings)
487 settings_maker = SettingsMaker(settings)
488
488
489 settings_maker.make_setting(
489 settings_maker.make_setting(
490 'logging.autoconfigure',
490 'logging.autoconfigure',
491 default=False,
491 default=False,
492 parser='bool')
492 parser='bool')
493
493
494 logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini')
494 logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini')
495 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
495 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
496
496
497 # Default includes, possible to change as a user
497 # Default includes, possible to change as a user
498 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
498 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
499 log.debug(
499 log.debug(
500 "Using the following pyramid.includes: %s",
500 "Using the following pyramid.includes: %s",
501 pyramid_includes)
501 pyramid_includes)
502
502
503 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
503 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
504 settings_maker.make_setting('rhodecode.edition_id', 'CE')
504 settings_maker.make_setting('rhodecode.edition_id', 'CE')
505
505
506 if 'mako.default_filters' not in settings:
506 if 'mako.default_filters' not in settings:
507 # set custom default filters if we don't have it defined
507 # set custom default filters if we don't have it defined
508 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
508 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
509 settings['mako.default_filters'] = 'h_filter'
509 settings['mako.default_filters'] = 'h_filter'
510
510
511 if 'mako.directories' not in settings:
511 if 'mako.directories' not in settings:
512 mako_directories = settings.setdefault('mako.directories', [
512 mako_directories = settings.setdefault('mako.directories', [
513 # Base templates of the original application
513 # Base templates of the original application
514 'rhodecode:templates',
514 'rhodecode:templates',
515 ])
515 ])
516 log.debug(
516 log.debug(
517 "Using the following Mako template directories: %s",
517 "Using the following Mako template directories: %s",
518 mako_directories)
518 mako_directories)
519
519
520 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
520 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
521 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
521 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
522 raw_url = settings['beaker.session.url']
522 raw_url = settings['beaker.session.url']
523 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
523 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
524 settings['beaker.session.url'] = 'redis://' + raw_url
524 settings['beaker.session.url'] = 'redis://' + raw_url
525
525
526 settings_maker.make_setting('__file__', global_config.get('__file__'))
526 settings_maker.make_setting('__file__', global_config.get('__file__'))
527
527
528 # TODO: johbo: Re-think this, usually the call to config.include
528 # TODO: johbo: Re-think this, usually the call to config.include
529 # should allow to pass in a prefix.
529 # should allow to pass in a prefix.
530 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
530 settings_maker.make_setting('rhodecode.api.url', api.DEFAULT_URL)
531
531
532 # Sanitize generic settings.
532 # Sanitize generic settings.
533 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
533 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
534 settings_maker.make_setting('is_test', False, parser='bool')
534 settings_maker.make_setting('is_test', False, parser='bool')
535 settings_maker.make_setting('gzip_responses', False, parser='bool')
535 settings_maker.make_setting('gzip_responses', False, parser='bool')
536
536
537 # statsd
537 # statsd
538 settings_maker.make_setting('statsd.enabled', False, parser='bool')
538 settings_maker.make_setting('statsd.enabled', False, parser='bool')
539 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
539 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
540 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
540 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
541 settings_maker.make_setting('statsd.statsd_prefix', '')
541 settings_maker.make_setting('statsd.statsd_prefix', '')
542 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
542 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
543
543
544 settings_maker.make_setting('vcs.svn.compatible_version', '')
544 settings_maker.make_setting('vcs.svn.compatible_version', '')
545 settings_maker.make_setting('vcs.hooks.protocol', 'http')
545 settings_maker.make_setting('vcs.hooks.protocol', 'http')
546 settings_maker.make_setting('vcs.hooks.host', '*')
546 settings_maker.make_setting('vcs.hooks.host', '*')
547 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
547 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
548 settings_maker.make_setting('vcs.server', '')
548 settings_maker.make_setting('vcs.server', '')
549 settings_maker.make_setting('vcs.server.protocol', 'http')
549 settings_maker.make_setting('vcs.server.protocol', 'http')
550 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
550 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
551 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
551 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
552 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
552 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
553 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
553 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
554 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
554 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
555 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
555 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
556
556
557 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
557 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
558
558
559 # Support legacy values of vcs.scm_app_implementation. Legacy
559 # Support legacy values of vcs.scm_app_implementation. Legacy
560 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
560 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
561 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
561 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
562 scm_app_impl = settings['vcs.scm_app_implementation']
562 scm_app_impl = settings['vcs.scm_app_implementation']
563 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
563 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
564 settings['vcs.scm_app_implementation'] = 'http'
564 settings['vcs.scm_app_implementation'] = 'http'
565
565
566 settings_maker.make_setting('appenlight', False, parser='bool')
566 settings_maker.make_setting('appenlight', False, parser='bool')
567
567
568 temp_store = tempfile.gettempdir()
568 temp_store = tempfile.gettempdir()
569 tmp_cache_dir = jn(temp_store, 'rc_cache')
569 tmp_cache_dir = jn(temp_store, 'rc_cache')
570
570
571 # save default, cache dir, and use it for all backends later.
571 # save default, cache dir, and use it for all backends later.
572 default_cache_dir = settings_maker.make_setting(
572 default_cache_dir = settings_maker.make_setting(
573 'cache_dir',
573 'cache_dir',
574 default=tmp_cache_dir, default_when_empty=True,
574 default=tmp_cache_dir, default_when_empty=True,
575 parser='dir:ensured')
575 parser='dir:ensured')
576
576
577 # exception store cache
577 # exception store cache
578 settings_maker.make_setting(
578 settings_maker.make_setting(
579 'exception_tracker.store_path',
579 'exception_tracker.store_path',
580 default=jn(default_cache_dir, 'exc_store'), default_when_empty=True,
580 default=jn(default_cache_dir, 'exc_store'), default_when_empty=True,
581 parser='dir:ensured'
581 parser='dir:ensured'
582 )
582 )
583
583
584 settings_maker.make_setting(
584 settings_maker.make_setting(
585 'celerybeat-schedule.path',
585 'celerybeat-schedule.path',
586 default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
586 default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
587 parser='file:ensured'
587 parser='file:ensured'
588 )
588 )
589
589
590 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
590 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
591 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
591 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
592
592
593 # sessions, ensure file since no-value is memory
593 # sessions, ensure file since no-value is memory
594 settings_maker.make_setting('beaker.session.type', 'file')
594 settings_maker.make_setting('beaker.session.type', 'file')
595 settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data'))
595 settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data'))
596
596
597 # cache_general
597 # cache_general
598 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
598 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
599 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
599 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
600 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db'))
600 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db'))
601
601
602 # cache_perms
602 # cache_perms
603 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
603 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
604 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
604 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
605 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db'))
605 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db'))
606
606
607 # cache_repo
607 # cache_repo
608 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
608 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
609 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
609 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
610 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db'))
610 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db'))
611
611
612 # cache_license
612 # cache_license
613 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
613 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
614 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
614 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
615 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db'))
615 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db'))
616
616
617 # cache_repo_longterm memory, 96H
617 # cache_repo_longterm memory, 96H
618 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
618 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
619 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
619 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
620 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
620 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
621
621
622 # sql_cache_short
622 # sql_cache_short
623 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
623 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
624 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
624 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
625 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
625 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
626
626
627 # archive_cache
627 # archive_cache
628 settings_maker.make_setting('archive_cache.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
628 settings_maker.make_setting('archive_cache.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
629 settings_maker.make_setting('archive_cache.cache_size_gb', 10, parser='float')
629 settings_maker.make_setting('archive_cache.cache_size_gb', 10, parser='float')
630 settings_maker.make_setting('archive_cache.cache_shards', 10, parser='int')
630 settings_maker.make_setting('archive_cache.cache_shards', 10, parser='int')
631
631
632 settings_maker.env_expand()
632 settings_maker.env_expand()
633
633
634 # configure instance id
634 # configure instance id
635 config_utils.set_instance_id(settings)
635 config_utils.set_instance_id(settings)
636
636
637 return settings
637 return settings
@@ -1,851 +1,857 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 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 """
19 """
20 Utilities library for RhodeCode
20 Utilities library for RhodeCode
21 """
21 """
22
22
23 import datetime
23 import datetime
24 import decorator
24 import decorator
25 import logging
25 import logging
26 import os
26 import os
27 import re
27 import re
28 import sys
28 import sys
29 import shutil
29 import shutil
30 import socket
30 import socket
31 import tempfile
31 import tempfile
32 import traceback
32 import traceback
33 import tarfile
33 import tarfile
34 import warnings
34 import warnings
35 from functools import wraps
35 from functools import wraps
36 from os.path import join as jn
36 from os.path import join as jn
37 from configparser import NoOptionError
37 from configparser import NoOptionError
38
38
39 import paste
39 import paste
40 import pkg_resources
40 import pkg_resources
41 from webhelpers2.text import collapse, strip_tags, convert_accented_entities, convert_misc_entities
41 from webhelpers2.text import collapse, strip_tags, convert_accented_entities, convert_misc_entities
42
42
43 from mako import exceptions
43 from mako import exceptions
44
44
45 from rhodecode.lib.hash_utils import sha256_safe, md5, sha1
45 from rhodecode.lib.hash_utils import sha256_safe, md5, sha1
46 from rhodecode.lib.type_utils import AttributeDict
46 from rhodecode.lib.type_utils import AttributeDict
47 from rhodecode.lib.str_utils import safe_bytes, safe_str
47 from rhodecode.lib.str_utils import safe_bytes, safe_str
48 from rhodecode.lib.vcs.backends.base import Config
48 from rhodecode.lib.vcs.backends.base import Config
49 from rhodecode.lib.vcs.exceptions import VCSError
49 from rhodecode.lib.vcs.exceptions import VCSError
50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 from rhodecode.lib.ext_json import sjson as json
51 from rhodecode.lib.ext_json import sjson as json
52 from rhodecode.model import meta
52 from rhodecode.model import meta
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
54 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.lib.pyramid_utils import get_config
56 from rhodecode.lib.pyramid_utils import get_config
57 from rhodecode.lib.vcs import CurlSession
57 from rhodecode.lib.vcs import CurlSession
58 from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError
58 from rhodecode.lib.vcs.exceptions import ImproperlyConfiguredError
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
63 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
64
64
65 # String which contains characters that are not allowed in slug names for
65 # String which contains characters that are not allowed in slug names for
66 # repositories or repository groups. It is properly escaped to use it in
66 # repositories or repository groups. It is properly escaped to use it in
67 # regular expressions.
67 # regular expressions.
68 SLUG_BAD_CHARS = re.escape(r'`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
68 SLUG_BAD_CHARS = re.escape(r'`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
69
69
70 # Regex that matches forbidden characters in repo/group slugs.
70 # Regex that matches forbidden characters in repo/group slugs.
71 SLUG_BAD_CHAR_RE = re.compile(r'[{}\x00-\x08\x0b-\x0c\x0e-\x1f]'.format(SLUG_BAD_CHARS))
71 SLUG_BAD_CHAR_RE = re.compile(r'[{}\x00-\x08\x0b-\x0c\x0e-\x1f]'.format(SLUG_BAD_CHARS))
72
72
73 # Regex that matches allowed characters in repo/group slugs.
73 # Regex that matches allowed characters in repo/group slugs.
74 SLUG_GOOD_CHAR_RE = re.compile(r'[^{}]'.format(SLUG_BAD_CHARS))
74 SLUG_GOOD_CHAR_RE = re.compile(r'[^{}]'.format(SLUG_BAD_CHARS))
75
75
76 # Regex that matches whole repo/group slugs.
76 # Regex that matches whole repo/group slugs.
77 SLUG_RE = re.compile(r'[^{}]+'.format(SLUG_BAD_CHARS))
77 SLUG_RE = re.compile(r'[^{}]+'.format(SLUG_BAD_CHARS))
78
78
79 _license_cache = None
79 _license_cache = None
80
80
81
81
82 def adopt_for_celery(func):
82 def adopt_for_celery(func):
83 """
83 """
84 Decorator designed to adopt hooks (from rhodecode.lib.hooks_base)
84 Decorator designed to adopt hooks (from rhodecode.lib.hooks_base)
85 for further usage as a celery tasks.
85 for further usage as a celery tasks.
86 """
86 """
87 @wraps(func)
87 @wraps(func)
88 def wrapper(extras):
88 def wrapper(extras):
89 extras = AttributeDict(extras)
89 extras = AttributeDict(extras)
90 # HooksResponse implements to_json method which must be used there.
90 # HooksResponse implements to_json method which must be used there.
91 return func(extras).to_json()
91 return func(extras).to_json()
92 return wrapper
92 return wrapper
93
93
94
94
95 def repo_name_slug(value):
95 def repo_name_slug(value):
96 """
96 """
97 Return slug of name of repository
97 Return slug of name of repository
98 This function is called on each creation/modification
98 This function is called on each creation/modification
99 of repository to prevent bad names in repo
99 of repository to prevent bad names in repo
100 """
100 """
101
101
102 replacement_char = '-'
102 replacement_char = '-'
103
103
104 slug = strip_tags(value)
104 slug = strip_tags(value)
105 slug = convert_accented_entities(slug)
105 slug = convert_accented_entities(slug)
106 slug = convert_misc_entities(slug)
106 slug = convert_misc_entities(slug)
107
107
108 slug = SLUG_BAD_CHAR_RE.sub('', slug)
108 slug = SLUG_BAD_CHAR_RE.sub('', slug)
109 slug = re.sub(r'[\s]+', '-', slug)
109 slug = re.sub(r'[\s]+', '-', slug)
110 slug = collapse(slug, replacement_char)
110 slug = collapse(slug, replacement_char)
111
111
112 return slug
112 return slug
113
113
114
114
115 #==============================================================================
115 #==============================================================================
116 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
116 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
117 #==============================================================================
117 #==============================================================================
118 def get_repo_slug(request):
118 def get_repo_slug(request):
119 _repo = ''
119 _repo = ''
120
120
121 if hasattr(request, 'db_repo_name'):
121 if hasattr(request, 'db_repo_name'):
122 # if our requests has set db reference use it for name, this
122 # if our requests has set db reference use it for name, this
123 # translates the example.com/_<id> into proper repo names
123 # translates the example.com/_<id> into proper repo names
124 _repo = request.db_repo_name
124 _repo = request.db_repo_name
125 elif getattr(request, 'matchdict', None):
125 elif getattr(request, 'matchdict', None):
126 # pyramid
126 # pyramid
127 _repo = request.matchdict.get('repo_name')
127 _repo = request.matchdict.get('repo_name')
128
128
129 if _repo:
129 if _repo:
130 _repo = _repo.rstrip('/')
130 _repo = _repo.rstrip('/')
131 return _repo
131 return _repo
132
132
133
133
134 def get_repo_group_slug(request):
134 def get_repo_group_slug(request):
135 _group = ''
135 _group = ''
136 if hasattr(request, 'db_repo_group'):
136 if hasattr(request, 'db_repo_group'):
137 # if our requests has set db reference use it for name, this
137 # if our requests has set db reference use it for name, this
138 # translates the example.com/_<id> into proper repo group names
138 # translates the example.com/_<id> into proper repo group names
139 _group = request.db_repo_group.group_name
139 _group = request.db_repo_group.group_name
140 elif getattr(request, 'matchdict', None):
140 elif getattr(request, 'matchdict', None):
141 # pyramid
141 # pyramid
142 _group = request.matchdict.get('repo_group_name')
142 _group = request.matchdict.get('repo_group_name')
143
143
144 if _group:
144 if _group:
145 _group = _group.rstrip('/')
145 _group = _group.rstrip('/')
146 return _group
146 return _group
147
147
148
148
149 def get_user_group_slug(request):
149 def get_user_group_slug(request):
150 _user_group = ''
150 _user_group = ''
151
151
152 if hasattr(request, 'db_user_group'):
152 if hasattr(request, 'db_user_group'):
153 _user_group = request.db_user_group.users_group_name
153 _user_group = request.db_user_group.users_group_name
154 elif getattr(request, 'matchdict', None):
154 elif getattr(request, 'matchdict', None):
155 # pyramid
155 # pyramid
156 _user_group = request.matchdict.get('user_group_id')
156 _user_group = request.matchdict.get('user_group_id')
157 _user_group_name = request.matchdict.get('user_group_name')
157 _user_group_name = request.matchdict.get('user_group_name')
158 try:
158 try:
159 if _user_group:
159 if _user_group:
160 _user_group = UserGroup.get(_user_group)
160 _user_group = UserGroup.get(_user_group)
161 elif _user_group_name:
161 elif _user_group_name:
162 _user_group = UserGroup.get_by_group_name(_user_group_name)
162 _user_group = UserGroup.get_by_group_name(_user_group_name)
163
163
164 if _user_group:
164 if _user_group:
165 _user_group = _user_group.users_group_name
165 _user_group = _user_group.users_group_name
166 except Exception:
166 except Exception:
167 log.exception('Failed to get user group by id and name')
167 log.exception('Failed to get user group by id and name')
168 # catch all failures here
168 # catch all failures here
169 return None
169 return None
170
170
171 return _user_group
171 return _user_group
172
172
173
173
174 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
174 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
175 """
175 """
176 Scans given path for repos and return (name,(type,path)) tuple
176 Scans given path for repos and return (name,(type,path)) tuple
177
177
178 :param path: path to scan for repositories
178 :param path: path to scan for repositories
179 :param recursive: recursive search and return names with subdirs in front
179 :param recursive: recursive search and return names with subdirs in front
180 """
180 """
181
181
182 # remove ending slash for better results
182 # remove ending slash for better results
183 path = path.rstrip(os.sep)
183 path = path.rstrip(os.sep)
184 log.debug('now scanning in %s location recursive:%s...', path, recursive)
184 log.debug('now scanning in %s location recursive:%s...', path, recursive)
185
185
186 def _get_repos(p):
186 def _get_repos(p):
187 dirpaths = get_dirpaths(p)
187 dirpaths = get_dirpaths(p)
188 if not _is_dir_writable(p):
188 if not _is_dir_writable(p):
189 log.warning('repo path without write access: %s', p)
189 log.warning('repo path without write access: %s', p)
190
190
191 for dirpath in dirpaths:
191 for dirpath in dirpaths:
192 if os.path.isfile(os.path.join(p, dirpath)):
192 if os.path.isfile(os.path.join(p, dirpath)):
193 continue
193 continue
194 cur_path = os.path.join(p, dirpath)
194 cur_path = os.path.join(p, dirpath)
195
195
196 # skip removed repos
196 # skip removed repos
197 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
197 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
198 continue
198 continue
199
199
200 #skip .<somethin> dirs
200 #skip .<somethin> dirs
201 if dirpath.startswith('.'):
201 if dirpath.startswith('.'):
202 continue
202 continue
203
203
204 try:
204 try:
205 scm_info = get_scm(cur_path)
205 scm_info = get_scm(cur_path)
206 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
206 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
207 except VCSError:
207 except VCSError:
208 if not recursive:
208 if not recursive:
209 continue
209 continue
210 #check if this dir containts other repos for recursive scan
210 #check if this dir containts other repos for recursive scan
211 rec_path = os.path.join(p, dirpath)
211 rec_path = os.path.join(p, dirpath)
212 if os.path.isdir(rec_path):
212 if os.path.isdir(rec_path):
213 yield from _get_repos(rec_path)
213 yield from _get_repos(rec_path)
214
214
215 return _get_repos(path)
215 return _get_repos(path)
216
216
217
217
218 def get_dirpaths(p: str) -> list:
218 def get_dirpaths(p: str) -> list:
219 try:
219 try:
220 # OS-independable way of checking if we have at least read-only
220 # OS-independable way of checking if we have at least read-only
221 # access or not.
221 # access or not.
222 dirpaths = os.listdir(p)
222 dirpaths = os.listdir(p)
223 except OSError:
223 except OSError:
224 log.warning('ignoring repo path without read access: %s', p)
224 log.warning('ignoring repo path without read access: %s', p)
225 return []
225 return []
226
226
227 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
227 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
228 # decode paths and suddenly returns unicode objects itself. The items it
228 # decode paths and suddenly returns unicode objects itself. The items it
229 # cannot decode are returned as strings and cause issues.
229 # cannot decode are returned as strings and cause issues.
230 #
230 #
231 # Those paths are ignored here until a solid solution for path handling has
231 # Those paths are ignored here until a solid solution for path handling has
232 # been built.
232 # been built.
233 expected_type = type(p)
233 expected_type = type(p)
234
234
235 def _has_correct_type(item):
235 def _has_correct_type(item):
236 if type(item) is not expected_type:
236 if type(item) is not expected_type:
237 log.error(
237 log.error(
238 "Ignoring path %s since it cannot be decoded into str.",
238 "Ignoring path %s since it cannot be decoded into str.",
239 # Using "repr" to make sure that we see the byte value in case
239 # Using "repr" to make sure that we see the byte value in case
240 # of support.
240 # of support.
241 repr(item))
241 repr(item))
242 return False
242 return False
243 return True
243 return True
244
244
245 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
245 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
246
246
247 return dirpaths
247 return dirpaths
248
248
249
249
250 def _is_dir_writable(path):
250 def _is_dir_writable(path):
251 """
251 """
252 Probe if `path` is writable.
252 Probe if `path` is writable.
253
253
254 Due to trouble on Cygwin / Windows, this is actually probing if it is
254 Due to trouble on Cygwin / Windows, this is actually probing if it is
255 possible to create a file inside of `path`, stat does not produce reliable
255 possible to create a file inside of `path`, stat does not produce reliable
256 results in this case.
256 results in this case.
257 """
257 """
258 try:
258 try:
259 with tempfile.TemporaryFile(dir=path):
259 with tempfile.TemporaryFile(dir=path):
260 pass
260 pass
261 except OSError:
261 except OSError:
262 return False
262 return False
263 return True
263 return True
264
264
265
265
266 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
266 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
267 """
267 """
268 Returns True if given path is a valid repository False otherwise.
268 Returns True if given path is a valid repository False otherwise.
269 If expect_scm param is given also, compare if given scm is the same
269 If expect_scm param is given also, compare if given scm is the same
270 as expected from scm parameter. If explicit_scm is given don't try to
270 as expected from scm parameter. If explicit_scm is given don't try to
271 detect the scm, just use the given one to check if repo is valid
271 detect the scm, just use the given one to check if repo is valid
272
272
273 :param repo_name:
273 :param repo_name:
274 :param base_path:
274 :param base_path:
275 :param expect_scm:
275 :param expect_scm:
276 :param explicit_scm:
276 :param explicit_scm:
277 :param config:
277 :param config:
278
278
279 :return True: if given path is a valid repository
279 :return True: if given path is a valid repository
280 """
280 """
281 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
281 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
282 log.debug('Checking if `%s` is a valid path for repository. '
282 log.debug('Checking if `%s` is a valid path for repository. '
283 'Explicit type: %s', repo_name, explicit_scm)
283 'Explicit type: %s', repo_name, explicit_scm)
284
284
285 try:
285 try:
286 if explicit_scm:
286 if explicit_scm:
287 detected_scms = [get_scm_backend(explicit_scm)(
287 detected_scms = [get_scm_backend(explicit_scm)(
288 full_path, config=config).alias]
288 full_path, config=config).alias]
289 else:
289 else:
290 detected_scms = get_scm(full_path)
290 detected_scms = get_scm(full_path)
291
291
292 if expect_scm:
292 if expect_scm:
293 return detected_scms[0] == expect_scm
293 return detected_scms[0] == expect_scm
294 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
294 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
295 return True
295 return True
296 except VCSError:
296 except VCSError:
297 log.debug('path: %s is not a valid repo !', full_path)
297 log.debug('path: %s is not a valid repo !', full_path)
298 return False
298 return False
299
299
300
300
301 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
301 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
302 """
302 """
303 Returns True if a given path is a repository group, False otherwise
303 Returns True if a given path is a repository group, False otherwise
304
304
305 :param repo_group_name:
305 :param repo_group_name:
306 :param base_path:
306 :param base_path:
307 """
307 """
308 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
308 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
309 log.debug('Checking if `%s` is a valid path for repository group',
309 log.debug('Checking if `%s` is a valid path for repository group',
310 repo_group_name)
310 repo_group_name)
311
311
312 # check if it's not a repo
312 # check if it's not a repo
313 if is_valid_repo(repo_group_name, base_path):
313 if is_valid_repo(repo_group_name, base_path):
314 log.debug('Repo called %s exist, it is not a valid repo group', repo_group_name)
314 log.debug('Repo called %s exist, it is not a valid repo group', repo_group_name)
315 return False
315 return False
316
316
317 try:
317 try:
318 # we need to check bare git repos at higher level
318 # we need to check bare git repos at higher level
319 # since we might match branches/hooks/info/objects or possible
319 # since we might match branches/hooks/info/objects or possible
320 # other things inside bare git repo
320 # other things inside bare git repo
321 maybe_repo = os.path.dirname(full_path)
321 maybe_repo = os.path.dirname(full_path)
322 if maybe_repo == base_path:
322 if maybe_repo == base_path:
323 # skip root level repo check; we know root location CANNOT BE a repo group
323 # skip root level repo check; we know root location CANNOT BE a repo group
324 return False
324 return False
325
325
326 scm_ = get_scm(maybe_repo)
326 scm_ = get_scm(maybe_repo)
327 log.debug('path: %s is a vcs object:%s, not valid repo group', full_path, scm_)
327 log.debug('path: %s is a vcs object:%s, not valid repo group', full_path, scm_)
328 return False
328 return False
329 except VCSError:
329 except VCSError:
330 pass
330 pass
331
331
332 # check if it's a valid path
332 # check if it's a valid path
333 if skip_path_check or os.path.isdir(full_path):
333 if skip_path_check or os.path.isdir(full_path):
334 log.debug('path: %s is a valid repo group !', full_path)
334 log.debug('path: %s is a valid repo group !', full_path)
335 return True
335 return True
336
336
337 log.debug('path: %s is not a valid repo group !', full_path)
337 log.debug('path: %s is not a valid repo group !', full_path)
338 return False
338 return False
339
339
340
340
341 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
341 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
342 while True:
342 while True:
343 ok = input(prompt)
343 ok = input(prompt)
344 if ok.lower() in ('y', 'ye', 'yes'):
344 if ok.lower() in ('y', 'ye', 'yes'):
345 return True
345 return True
346 if ok.lower() in ('n', 'no', 'nop', 'nope'):
346 if ok.lower() in ('n', 'no', 'nop', 'nope'):
347 return False
347 return False
348 retries = retries - 1
348 retries = retries - 1
349 if retries < 0:
349 if retries < 0:
350 raise OSError
350 raise OSError
351 print(complaint)
351 print(complaint)
352
352
353 # propagated from mercurial documentation
353 # propagated from mercurial documentation
354 ui_sections = [
354 ui_sections = [
355 'alias', 'auth',
355 'alias', 'auth',
356 'decode/encode', 'defaults',
356 'decode/encode', 'defaults',
357 'diff', 'email',
357 'diff', 'email',
358 'extensions', 'format',
358 'extensions', 'format',
359 'merge-patterns', 'merge-tools',
359 'merge-patterns', 'merge-tools',
360 'hooks', 'http_proxy',
360 'hooks', 'http_proxy',
361 'smtp', 'patch',
361 'smtp', 'patch',
362 'paths', 'profiling',
362 'paths', 'profiling',
363 'server', 'trusted',
363 'server', 'trusted',
364 'ui', 'web', ]
364 'ui', 'web', ]
365
365
366
366
367 def config_data_from_db(clear_session=True, repo=None):
367 def config_data_from_db(clear_session=True, repo=None):
368 """
368 """
369 Read the configuration data from the database and return configuration
369 Read the configuration data from the database and return configuration
370 tuples.
370 tuples.
371 """
371 """
372 from rhodecode.model.settings import VcsSettingsModel
372 from rhodecode.model.settings import VcsSettingsModel
373
373
374 config = []
374 config = []
375
375
376 sa = meta.Session()
376 sa = meta.Session()
377 settings_model = VcsSettingsModel(repo=repo, sa=sa)
377 settings_model = VcsSettingsModel(repo=repo, sa=sa)
378
378
379 ui_settings = settings_model.get_ui_settings()
379 ui_settings = settings_model.get_ui_settings()
380
380
381 ui_data = []
381 ui_data = []
382 for setting in ui_settings:
382 for setting in ui_settings:
383 if setting.active:
383 if setting.active:
384 ui_data.append((setting.section, setting.key, setting.value))
384 ui_data.append((setting.section, setting.key, setting.value))
385 config.append((
385 config.append((
386 safe_str(setting.section), safe_str(setting.key),
386 safe_str(setting.section), safe_str(setting.key),
387 safe_str(setting.value)))
387 safe_str(setting.value)))
388 if setting.key == 'push_ssl':
388 if setting.key == 'push_ssl':
389 # force set push_ssl requirement to False, rhodecode
389 # force set push_ssl requirement to False, rhodecode
390 # handles that
390 # handles that
391 config.append((
391 config.append((
392 safe_str(setting.section), safe_str(setting.key), False))
392 safe_str(setting.section), safe_str(setting.key), False))
393 log.debug(
393 log.debug(
394 'settings ui from db@repo[%s]: %s',
394 'settings ui from db@repo[%s]: %s',
395 repo,
395 repo,
396 ','.join(['[{}] {}={}'.format(*s) for s in ui_data]))
396 ','.join(['[{}] {}={}'.format(*s) for s in ui_data]))
397 if clear_session:
397 if clear_session:
398 meta.Session.remove()
398 meta.Session.remove()
399
399
400 # TODO: mikhail: probably it makes no sense to re-read hooks information.
400 # TODO: mikhail: probably it makes no sense to re-read hooks information.
401 # It's already there and activated/deactivated
401 # It's already there and activated/deactivated
402 skip_entries = []
402 skip_entries = []
403 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
403 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
404 if 'pull' not in enabled_hook_classes:
404 if 'pull' not in enabled_hook_classes:
405 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
405 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
406 if 'push' not in enabled_hook_classes:
406 if 'push' not in enabled_hook_classes:
407 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
407 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
408 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
408 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
409 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
409 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
410
410
411 config = [entry for entry in config if entry[:2] not in skip_entries]
411 config = [entry for entry in config if entry[:2] not in skip_entries]
412
412
413 return config
413 return config
414
414
415
415
416 def make_db_config(clear_session=True, repo=None):
416 def make_db_config(clear_session=True, repo=None):
417 """
417 """
418 Create a :class:`Config` instance based on the values in the database.
418 Create a :class:`Config` instance based on the values in the database.
419 """
419 """
420 config = Config()
420 config = Config()
421 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
421 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
422 for section, option, value in config_data:
422 for section, option, value in config_data:
423 config.set(section, option, value)
423 config.set(section, option, value)
424 return config
424 return config
425
425
426
426
427 def get_enabled_hook_classes(ui_settings):
427 def get_enabled_hook_classes(ui_settings):
428 """
428 """
429 Return the enabled hook classes.
429 Return the enabled hook classes.
430
430
431 :param ui_settings: List of ui_settings as returned
431 :param ui_settings: List of ui_settings as returned
432 by :meth:`VcsSettingsModel.get_ui_settings`
432 by :meth:`VcsSettingsModel.get_ui_settings`
433
433
434 :return: a list with the enabled hook classes. The order is not guaranteed.
434 :return: a list with the enabled hook classes. The order is not guaranteed.
435 :rtype: list
435 :rtype: list
436 """
436 """
437 enabled_hooks = []
437 enabled_hooks = []
438 active_hook_keys = [
438 active_hook_keys = [
439 key for section, key, value, active in ui_settings
439 key for section, key, value, active in ui_settings
440 if section == 'hooks' and active]
440 if section == 'hooks' and active]
441
441
442 hook_names = {
442 hook_names = {
443 RhodeCodeUi.HOOK_PUSH: 'push',
443 RhodeCodeUi.HOOK_PUSH: 'push',
444 RhodeCodeUi.HOOK_PULL: 'pull',
444 RhodeCodeUi.HOOK_PULL: 'pull',
445 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
445 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
446 }
446 }
447
447
448 for key in active_hook_keys:
448 for key in active_hook_keys:
449 hook = hook_names.get(key)
449 hook = hook_names.get(key)
450 if hook:
450 if hook:
451 enabled_hooks.append(hook)
451 enabled_hooks.append(hook)
452
452
453 return enabled_hooks
453 return enabled_hooks
454
454
455
455
456 def set_rhodecode_config(config):
456 def set_rhodecode_config(config):
457 """
457 """
458 Updates pyramid config with new settings from database
458 Updates pyramid config with new settings from database
459
459
460 :param config:
460 :param config:
461 """
461 """
462 from rhodecode.model.settings import SettingsModel
462 from rhodecode.model.settings import SettingsModel
463 app_settings = SettingsModel().get_all_settings()
463 app_settings = SettingsModel().get_all_settings()
464
464
465 for k, v in list(app_settings.items()):
465 for k, v in list(app_settings.items()):
466 config[k] = v
466 config[k] = v
467
467
468
468
469 def get_rhodecode_realm():
469 def get_rhodecode_realm():
470 """
470 """
471 Return the rhodecode realm from database.
471 Return the rhodecode realm from database.
472 """
472 """
473 from rhodecode.model.settings import SettingsModel
473 from rhodecode.model.settings import SettingsModel
474 realm = SettingsModel().get_setting_by_name('realm')
474 realm = SettingsModel().get_setting_by_name('realm')
475 return safe_str(realm.app_settings_value)
475 return safe_str(realm.app_settings_value)
476
476
477
477
478 def get_rhodecode_base_path():
478 def get_rhodecode_base_path():
479 """
479 """
480 Returns the base path. The base path is the filesystem path which points
480 Returns the base path. The base path is the filesystem path which points
481 to the repository store.
481 to the repository store.
482 """
482 """
483
483
484 import rhodecode
484 import rhodecode
485 return rhodecode.CONFIG['default_base_path']
485 return rhodecode.CONFIG['default_base_path']
486
486
487
487
488 def map_groups(path):
488 def map_groups(path):
489 """
489 """
490 Given a full path to a repository, create all nested groups that this
490 Given a full path to a repository, create all nested groups that this
491 repo is inside. This function creates parent-child relationships between
491 repo is inside. This function creates parent-child relationships between
492 groups and creates default perms for all new groups.
492 groups and creates default perms for all new groups.
493
493
494 :param paths: full path to repository
494 :param paths: full path to repository
495 """
495 """
496 from rhodecode.model.repo_group import RepoGroupModel
496 from rhodecode.model.repo_group import RepoGroupModel
497 sa = meta.Session()
497 sa = meta.Session()
498 groups = path.split(Repository.NAME_SEP)
498 groups = path.split(Repository.NAME_SEP)
499 parent = None
499 parent = None
500 group = None
500 group = None
501
501
502 # last element is repo in nested groups structure
502 # last element is repo in nested groups structure
503 groups = groups[:-1]
503 groups = groups[:-1]
504 rgm = RepoGroupModel(sa)
504 rgm = RepoGroupModel(sa)
505 owner = User.get_first_super_admin()
505 owner = User.get_first_super_admin()
506 for lvl, group_name in enumerate(groups):
506 for lvl, group_name in enumerate(groups):
507 group_name = '/'.join(groups[:lvl] + [group_name])
507 group_name = '/'.join(groups[:lvl] + [group_name])
508 group = RepoGroup.get_by_group_name(group_name)
508 group = RepoGroup.get_by_group_name(group_name)
509 desc = '%s group' % group_name
509 desc = '%s group' % group_name
510
510
511 # skip folders that are now removed repos
511 # skip folders that are now removed repos
512 if REMOVED_REPO_PAT.match(group_name):
512 if REMOVED_REPO_PAT.match(group_name):
513 break
513 break
514
514
515 if group is None:
515 if group is None:
516 log.debug('creating group level: %s group_name: %s',
516 log.debug('creating group level: %s group_name: %s',
517 lvl, group_name)
517 lvl, group_name)
518 group = RepoGroup(group_name, parent)
518 group = RepoGroup(group_name, parent)
519 group.group_description = desc
519 group.group_description = desc
520 group.user = owner
520 group.user = owner
521 sa.add(group)
521 sa.add(group)
522 perm_obj = rgm._create_default_perms(group)
522 perm_obj = rgm._create_default_perms(group)
523 sa.add(perm_obj)
523 sa.add(perm_obj)
524 sa.flush()
524 sa.flush()
525
525
526 parent = group
526 parent = group
527 return group
527 return group
528
528
529
529
530 def repo2db_mapper(initial_repo_list, remove_obsolete=False, force_hooks_rebuild=False):
530 def repo2db_mapper(initial_repo_list, remove_obsolete=False, force_hooks_rebuild=False):
531 """
531 """
532 maps all repos given in initial_repo_list, non existing repositories
532 maps all repos given in initial_repo_list, non existing repositories
533 are created, if remove_obsolete is True it also checks for db entries
533 are created, if remove_obsolete is True it also checks for db entries
534 that are not in initial_repo_list and removes them.
534 that are not in initial_repo_list and removes them.
535
535
536 :param initial_repo_list: list of repositories found by scanning methods
536 :param initial_repo_list: list of repositories found by scanning methods
537 :param remove_obsolete: check for obsolete entries in database
537 :param remove_obsolete: check for obsolete entries in database
538 """
538 """
539 from rhodecode.model.repo import RepoModel
539 from rhodecode.model.repo import RepoModel
540 from rhodecode.model.repo_group import RepoGroupModel
540 from rhodecode.model.repo_group import RepoGroupModel
541 from rhodecode.model.settings import SettingsModel
541 from rhodecode.model.settings import SettingsModel
542
542
543 sa = meta.Session()
543 sa = meta.Session()
544 repo_model = RepoModel()
544 repo_model = RepoModel()
545 user = User.get_first_super_admin()
545 user = User.get_first_super_admin()
546 added = []
546 added = []
547
547
548 # creation defaults
548 # creation defaults
549 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
549 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
550 enable_statistics = defs.get('repo_enable_statistics')
550 enable_statistics = defs.get('repo_enable_statistics')
551 enable_locking = defs.get('repo_enable_locking')
551 enable_locking = defs.get('repo_enable_locking')
552 enable_downloads = defs.get('repo_enable_downloads')
552 enable_downloads = defs.get('repo_enable_downloads')
553 private = defs.get('repo_private')
553 private = defs.get('repo_private')
554
554
555 for name, repo in list(initial_repo_list.items()):
555 for name, repo in list(initial_repo_list.items()):
556 group = map_groups(name)
556 group = map_groups(name)
557 str_name = safe_str(name)
557 str_name = safe_str(name)
558 db_repo = repo_model.get_by_repo_name(str_name)
558 db_repo = repo_model.get_by_repo_name(str_name)
559
559
560 # found repo that is on filesystem not in RhodeCode database
560 # found repo that is on filesystem not in RhodeCode database
561 if not db_repo:
561 if not db_repo:
562 log.info('repository `%s` not found in the database, creating now', name)
562 log.info('repository `%s` not found in the database, creating now', name)
563 added.append(name)
563 added.append(name)
564 desc = (repo.description
564 desc = (repo.description
565 if repo.description != 'unknown'
565 if repo.description != 'unknown'
566 else '%s repository' % name)
566 else '%s repository' % name)
567
567
568 db_repo = repo_model._create_repo(
568 db_repo = repo_model._create_repo(
569 repo_name=name,
569 repo_name=name,
570 repo_type=repo.alias,
570 repo_type=repo.alias,
571 description=desc,
571 description=desc,
572 repo_group=getattr(group, 'group_id', None),
572 repo_group=getattr(group, 'group_id', None),
573 owner=user,
573 owner=user,
574 enable_locking=enable_locking,
574 enable_locking=enable_locking,
575 enable_downloads=enable_downloads,
575 enable_downloads=enable_downloads,
576 enable_statistics=enable_statistics,
576 enable_statistics=enable_statistics,
577 private=private,
577 private=private,
578 state=Repository.STATE_CREATED
578 state=Repository.STATE_CREATED
579 )
579 )
580 sa.commit()
580 sa.commit()
581 # we added that repo just now, and make sure we updated server info
581 # we added that repo just now, and make sure we updated server info
582 if db_repo.repo_type == 'git':
582 if db_repo.repo_type == 'git':
583 git_repo = db_repo.scm_instance()
583 git_repo = db_repo.scm_instance()
584 # update repository server-info
584 # update repository server-info
585 log.debug('Running update server info')
585 log.debug('Running update server info')
586 git_repo._update_server_info(force=True)
586 git_repo._update_server_info(force=True)
587
587
588 db_repo.update_commit_cache()
588 db_repo.update_commit_cache()
589
589
590 config = db_repo._config
590 config = db_repo._config
591 config.set('extensions', 'largefiles', '')
591 config.set('extensions', 'largefiles', '')
592 repo = db_repo.scm_instance(config=config)
592 repo = db_repo.scm_instance(config=config)
593 repo.install_hooks(force=force_hooks_rebuild)
593 repo.install_hooks(force=force_hooks_rebuild)
594
594
595 removed = []
595 removed = []
596 if remove_obsolete:
596 if remove_obsolete:
597 # remove from database those repositories that are not in the filesystem
597 # remove from database those repositories that are not in the filesystem
598 for repo in sa.query(Repository).all():
598 for repo in sa.query(Repository).all():
599 if repo.repo_name not in list(initial_repo_list.keys()):
599 if repo.repo_name not in list(initial_repo_list.keys()):
600 log.debug("Removing non-existing repository found in db `%s`",
600 log.debug("Removing non-existing repository found in db `%s`",
601 repo.repo_name)
601 repo.repo_name)
602 try:
602 try:
603 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
603 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
604 sa.commit()
604 sa.commit()
605 removed.append(repo.repo_name)
605 removed.append(repo.repo_name)
606 except Exception:
606 except Exception:
607 # don't hold further removals on error
607 # don't hold further removals on error
608 log.error(traceback.format_exc())
608 log.error(traceback.format_exc())
609 sa.rollback()
609 sa.rollback()
610
610
611 def splitter(full_repo_name):
611 def splitter(full_repo_name):
612 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
612 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
613 gr_name = None
613 gr_name = None
614 if len(_parts) == 2:
614 if len(_parts) == 2:
615 gr_name = _parts[0]
615 gr_name = _parts[0]
616 return gr_name
616 return gr_name
617
617
618 initial_repo_group_list = [splitter(x) for x in
618 initial_repo_group_list = [splitter(x) for x in
619 list(initial_repo_list.keys()) if splitter(x)]
619 list(initial_repo_list.keys()) if splitter(x)]
620
620
621 # remove from database those repository groups that are not in the
621 # remove from database those repository groups that are not in the
622 # filesystem due to parent child relationships we need to delete them
622 # filesystem due to parent child relationships we need to delete them
623 # in a specific order of most nested first
623 # in a specific order of most nested first
624 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
624 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
625 def nested_sort(gr):
625 def nested_sort(gr):
626 return len(gr.split('/'))
626 return len(gr.split('/'))
627 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
627 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
628 if group_name not in initial_repo_group_list:
628 if group_name not in initial_repo_group_list:
629 repo_group = RepoGroup.get_by_group_name(group_name)
629 repo_group = RepoGroup.get_by_group_name(group_name)
630 if (repo_group.children.all() or
630 if (repo_group.children.all() or
631 not RepoGroupModel().check_exist_filesystem(
631 not RepoGroupModel().check_exist_filesystem(
632 group_name=group_name, exc_on_failure=False)):
632 group_name=group_name, exc_on_failure=False)):
633 continue
633 continue
634
634
635 log.info(
635 log.info(
636 'Removing non-existing repository group found in db `%s`',
636 'Removing non-existing repository group found in db `%s`',
637 group_name)
637 group_name)
638 try:
638 try:
639 RepoGroupModel(sa).delete(group_name, fs_remove=False)
639 RepoGroupModel(sa).delete(group_name, fs_remove=False)
640 sa.commit()
640 sa.commit()
641 removed.append(group_name)
641 removed.append(group_name)
642 except Exception:
642 except Exception:
643 # don't hold further removals on error
643 # don't hold further removals on error
644 log.exception(
644 log.exception(
645 'Unable to remove repository group `%s`',
645 'Unable to remove repository group `%s`',
646 group_name)
646 group_name)
647 sa.rollback()
647 sa.rollback()
648 raise
648 raise
649
649
650 return added, removed
650 return added, removed
651
651
652
652
653 def load_rcextensions(root_path):
653 def load_rcextensions(root_path):
654 import rhodecode
654 import rhodecode
655 from rhodecode.config import conf
655 from rhodecode.config import conf
656
656
657 path = os.path.join(root_path)
657 path = os.path.join(root_path)
658 sys.path.append(path)
658 sys.path.append(path)
659
659
660 try:
660 try:
661 rcextensions = __import__('rcextensions')
661 rcextensions = __import__('rcextensions')
662 except ImportError:
662 except ImportError:
663 if os.path.isdir(os.path.join(path, 'rcextensions')):
663 if os.path.isdir(os.path.join(path, 'rcextensions')):
664 log.warning('Unable to load rcextensions from %s', path)
664 log.warning('Unable to load rcextensions from %s', path)
665 rcextensions = None
665 rcextensions = None
666
666
667 if rcextensions:
667 if rcextensions:
668 log.info('Loaded rcextensions from %s...', rcextensions)
668 log.info('Loaded rcextensions from %s...', rcextensions)
669 rhodecode.EXTENSIONS = rcextensions
669 rhodecode.EXTENSIONS = rcextensions
670
670
671 # Additional mappings that are not present in the pygments lexers
671 # Additional mappings that are not present in the pygments lexers
672 conf.LANGUAGES_EXTENSIONS_MAP.update(
672 conf.LANGUAGES_EXTENSIONS_MAP.update(
673 getattr(rhodecode.EXTENSIONS, 'EXTRA_MAPPINGS', {}))
673 getattr(rhodecode.EXTENSIONS, 'EXTRA_MAPPINGS', {}))
674
674
675
675
676 def get_custom_lexer(extension):
676 def get_custom_lexer(extension):
677 """
677 """
678 returns a custom lexer if it is defined in rcextensions module, or None
678 returns a custom lexer if it is defined in rcextensions module, or None
679 if there's no custom lexer defined
679 if there's no custom lexer defined
680 """
680 """
681 import rhodecode
681 import rhodecode
682 from pygments import lexers
682 from pygments import lexers
683
683
684 # custom override made by RhodeCode
684 # custom override made by RhodeCode
685 if extension in ['mako']:
685 if extension in ['mako']:
686 return lexers.get_lexer_by_name('html+mako')
686 return lexers.get_lexer_by_name('html+mako')
687
687
688 # check if we didn't define this extension as other lexer
688 # check if we didn't define this extension as other lexer
689 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
689 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
690 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
690 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
691 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
691 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
692 return lexers.get_lexer_by_name(_lexer_name)
692 return lexers.get_lexer_by_name(_lexer_name)
693
693
694
694
695 #==============================================================================
695 #==============================================================================
696 # TEST FUNCTIONS AND CREATORS
696 # TEST FUNCTIONS AND CREATORS
697 #==============================================================================
697 #==============================================================================
698 def create_test_index(repo_location, config):
698 def create_test_index(repo_location, config):
699 """
699 """
700 Makes default test index.
700 Makes default test index.
701 """
701 """
702 try:
702 try:
703 import rc_testdata
703 import rc_testdata
704 except ImportError:
704 except ImportError:
705 raise ImportError('Failed to import rc_testdata, '
705 raise ImportError('Failed to import rc_testdata, '
706 'please make sure this package is installed from requirements_test.txt')
706 'please make sure this package is installed from requirements_test.txt')
707 rc_testdata.extract_search_index(
707 rc_testdata.extract_search_index(
708 'vcs_search_index', os.path.dirname(config['search.location']))
708 'vcs_search_index', os.path.dirname(config['search.location']))
709
709
710
710
711 def create_test_directory(test_path):
711 def create_test_directory(test_path):
712 """
712 """
713 Create test directory if it doesn't exist.
713 Create test directory if it doesn't exist.
714 """
714 """
715 if not os.path.isdir(test_path):
715 if not os.path.isdir(test_path):
716 log.debug('Creating testdir %s', test_path)
716 log.debug('Creating testdir %s', test_path)
717 os.makedirs(test_path)
717 os.makedirs(test_path)
718
718
719
719
720 def create_test_database(test_path, config):
720 def create_test_database(test_path, config):
721 """
721 """
722 Makes a fresh database.
722 Makes a fresh database.
723 """
723 """
724 from rhodecode.lib.db_manage import DbManage
724 from rhodecode.lib.db_manage import DbManage
725 from rhodecode.lib.utils2 import get_encryption_key
725 from rhodecode.lib.utils2 import get_encryption_key
726
726
727 # PART ONE create db
727 # PART ONE create db
728 dbconf = config['sqlalchemy.db1.url']
728 dbconf = config['sqlalchemy.db1.url']
729 enc_key = get_encryption_key(config)
729 enc_key = get_encryption_key(config)
730
730
731 log.debug('making test db %s', dbconf)
731 log.debug('making test db %s', dbconf)
732
732
733 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
733 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
734 tests=True, cli_args={'force_ask': True}, enc_key=enc_key)
734 tests=True, cli_args={'force_ask': True}, enc_key=enc_key)
735 dbmanage.create_tables(override=True)
735 dbmanage.create_tables(override=True)
736 dbmanage.set_db_version()
736 dbmanage.set_db_version()
737 # for tests dynamically set new root paths based on generated content
737 # for tests dynamically set new root paths based on generated content
738 dbmanage.create_settings(dbmanage.config_prompt(test_path))
738 dbmanage.create_settings(dbmanage.config_prompt(test_path))
739 dbmanage.create_default_user()
739 dbmanage.create_default_user()
740 dbmanage.create_test_admin_and_users()
740 dbmanage.create_test_admin_and_users()
741 dbmanage.create_permissions()
741 dbmanage.create_permissions()
742 dbmanage.populate_default_permissions()
742 dbmanage.populate_default_permissions()
743 Session().commit()
743 Session().commit()
744
744
745
745
746 def create_test_repositories(test_path, config):
746 def create_test_repositories(test_path, config):
747 """
747 """
748 Creates test repositories in the temporary directory. Repositories are
748 Creates test repositories in the temporary directory. Repositories are
749 extracted from archives within the rc_testdata package.
749 extracted from archives within the rc_testdata package.
750 """
750 """
751 import rc_testdata
751 import rc_testdata
752 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
752 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
753
753
754 log.debug('making test vcs repositories')
754 log.debug('making test vcs repositories')
755
755
756 idx_path = config['search.location']
756 idx_path = config['search.location']
757 data_path = config['cache_dir']
757 data_path = config['cache_dir']
758
758
759 # clean index and data
759 # clean index and data
760 if idx_path and os.path.exists(idx_path):
760 if idx_path and os.path.exists(idx_path):
761 log.debug('remove %s', idx_path)
761 log.debug('remove %s', idx_path)
762 shutil.rmtree(idx_path)
762 shutil.rmtree(idx_path)
763
763
764 if data_path and os.path.exists(data_path):
764 if data_path and os.path.exists(data_path):
765 log.debug('remove %s', data_path)
765 log.debug('remove %s', data_path)
766 shutil.rmtree(data_path)
766 shutil.rmtree(data_path)
767
767
768 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
768 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
769 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
769 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
770
770
771 # Note: Subversion is in the process of being integrated with the system,
771 # Note: Subversion is in the process of being integrated with the system,
772 # until we have a properly packed version of the test svn repository, this
772 # until we have a properly packed version of the test svn repository, this
773 # tries to copy over the repo from a package "rc_testdata"
773 # tries to copy over the repo from a package "rc_testdata"
774 svn_repo_path = rc_testdata.get_svn_repo_archive()
774 svn_repo_path = rc_testdata.get_svn_repo_archive()
775 with tarfile.open(svn_repo_path) as tar:
775 with tarfile.open(svn_repo_path) as tar:
776 tar.extractall(jn(test_path, SVN_REPO))
776 tar.extractall(jn(test_path, SVN_REPO))
777
777
778
778
779 def password_changed(auth_user, session):
779 def password_changed(auth_user, session):
780 # Never report password change in case of default user or anonymous user.
780 # Never report password change in case of default user or anonymous user.
781 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
781 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
782 return False
782 return False
783
783
784 password_hash = md5(safe_bytes(auth_user.password)) if auth_user.password else None
784 password_hash = md5(safe_bytes(auth_user.password)) if auth_user.password else None
785 rhodecode_user = session.get('rhodecode_user', {})
785 rhodecode_user = session.get('rhodecode_user', {})
786 session_password_hash = rhodecode_user.get('password', '')
786 session_password_hash = rhodecode_user.get('password', '')
787 return password_hash != session_password_hash
787 return password_hash != session_password_hash
788
788
789
789
790 def read_opensource_licenses():
790 def read_opensource_licenses():
791 global _license_cache
791 global _license_cache
792
792
793 if not _license_cache:
793 if not _license_cache:
794 licenses = pkg_resources.resource_string(
794 licenses = pkg_resources.resource_string(
795 'rhodecode', 'config/licenses.json')
795 'rhodecode', 'config/licenses.json')
796 _license_cache = json.loads(licenses)
796 _license_cache = json.loads(licenses)
797
797
798 return _license_cache
798 return _license_cache
799
799
800
800
801 def generate_platform_uuid():
801 def generate_platform_uuid():
802 """
802 """
803 Generates platform UUID based on it's name
803 Generates platform UUID based on it's name
804 """
804 """
805 import platform
805 import platform
806
806
807 try:
807 try:
808 uuid_list = [platform.platform()]
808 uuid_list = [platform.platform()]
809 return sha256_safe(':'.join(uuid_list))
809 return sha256_safe(':'.join(uuid_list))
810 except Exception as e:
810 except Exception as e:
811 log.error('Failed to generate host uuid: %s', e)
811 log.error('Failed to generate host uuid: %s', e)
812 return 'UNDEFINED'
812 return 'UNDEFINED'
813
813
814
814
815 def send_test_email(recipients, email_body='TEST EMAIL'):
815 def send_test_email(recipients, email_body='TEST EMAIL'):
816 """
816 """
817 Simple code for generating test emails.
817 Simple code for generating test emails.
818 Usage::
818 Usage::
819
819
820 from rhodecode.lib import utils
820 from rhodecode.lib import utils
821 utils.send_test_email()
821 utils.send_test_email()
822 """
822 """
823 from rhodecode.lib.celerylib import tasks, run_task
823 from rhodecode.lib.celerylib import tasks, run_task
824
824
825 email_body = email_body_plaintext = email_body
825 email_body = email_body_plaintext = email_body
826 subject = f'SUBJECT FROM: {socket.gethostname()}'
826 subject = f'SUBJECT FROM: {socket.gethostname()}'
827 tasks.send_email(recipients, subject, email_body_plaintext, email_body)
827 tasks.send_email(recipients, subject, email_body_plaintext, email_body)
828
828
829
829
830 def call_service_api(ini_path, payload):
830 def call_service_api(ini_path, payload):
831 config = get_config(ini_path)
831 config = get_config(ini_path)
832 try:
832 try:
833 host = config.get('app:main', 'app.service_api.host')
833 host = config.get('app:main', 'app.service_api.host')
834 except NoOptionError:
834 except NoOptionError:
835 raise ImproperlyConfiguredError(
835 raise ImproperlyConfiguredError(
836 "app.service_api.host is missing. "
836 "app.service_api.host is missing. "
837 "Please ensure that app.service_api.host and app.service_api.token are "
837 "Please ensure that app.service_api.host and app.service_api.token are "
838 "defined inside of .ini configuration file."
838 "defined inside of .ini configuration file."
839 )
839 )
840 api_url = config.get('app:main', 'rhodecode.api.url')
840 try:
841 api_url = config.get('app:main', 'rhodecode.api.url')
842 except NoOptionError:
843 from rhodecode import api
844 log.debug('Cannot find rhodecode.api.url, setting API URL TO Default value')
845 api_url = api.DEFAULT_URL
846
841 payload.update({
847 payload.update({
842 'id': 'service',
848 'id': 'service',
843 'auth_token': config.get('app:main', 'app.service_api.token')
849 'auth_token': config.get('app:main', 'app.service_api.token')
844 })
850 })
845
851
846 response = CurlSession().post(f'{host}{api_url}', json.dumps(payload))
852 response = CurlSession().post(f'{host}{api_url}', json.dumps(payload))
847
853
848 if response.status_code != 200:
854 if response.status_code != 200:
849 raise Exception("Service API responded with error")
855 raise Exception("Service API responded with error")
850
856
851 return json.loads(response.content)['result']
857 return json.loads(response.content)['result']
General Comments 0
You need to be logged in to leave comments. Login now