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