##// END OF EJS Templates
exceptions: skip extracting exception by deprecated .message attribute
super-admin -
r5104:02a62824 default
parent child Browse files
Show More
@@ -1,574 +1,574 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 25 import typing
26 26 import venusian
27 27 from collections import OrderedDict
28 28
29 29 from pyramid.exceptions import ConfigurationError
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32 from pyramid.httpexceptions import HTTPNotFound
33 33
34 34 from rhodecode.api.exc import (
35 35 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
36 36 from rhodecode.apps._base import TemplateArgs
37 37 from rhodecode.lib.auth import AuthUser
38 38 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
39 39 from rhodecode.lib.exc_tracking import store_exception
40 40 from rhodecode.lib import ext_json
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.lib.plugins.utils import get_plugin_settings
43 43 from rhodecode.model.db import User, UserApiKeys
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 48 DEFAULT_URL = '/_admin/apiv2'
49 49
50 50
51 51 def find_methods(jsonrpc_methods, pattern):
52 52 matches = OrderedDict()
53 53 if not isinstance(pattern, (list, tuple)):
54 54 pattern = [pattern]
55 55
56 56 for single_pattern in pattern:
57 57 for method_name, method in jsonrpc_methods.items():
58 58 if fnmatch.fnmatch(method_name, single_pattern):
59 59 matches[method_name] = method
60 60 return matches
61 61
62 62
63 63 class ExtJsonRenderer(object):
64 64 """
65 65 Custom renderer that makes use of our ext_json lib
66 66
67 67 """
68 68
69 69 def __init__(self):
70 70 self.serializer = ext_json.formatted_json
71 71
72 72 def __call__(self, info):
73 73 """ Returns a plain JSON-encoded string with content-type
74 74 ``application/json``. The content-type may be overridden by
75 75 setting ``request.response.content_type``."""
76 76
77 77 def _render(value, system):
78 78 request = system.get('request')
79 79 if request is not None:
80 80 response = request.response
81 81 ct = response.content_type
82 82 if ct == response.default_content_type:
83 83 response.content_type = 'application/json'
84 84
85 85 return self.serializer(value)
86 86
87 87 return _render
88 88
89 89
90 90 def jsonrpc_response(request, result):
91 91 rpc_id = getattr(request, 'rpc_id', None)
92 92
93 93 ret_value = ''
94 94 if rpc_id:
95 95 ret_value = {'id': rpc_id, 'result': result, 'error': None}
96 96
97 97 # fetch deprecation warnings, and store it inside results
98 98 deprecation = getattr(request, 'rpc_deprecation', None)
99 99 if deprecation:
100 100 ret_value['DEPRECATION_WARNING'] = deprecation
101 101
102 102 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
103 103 content_type = 'application/json'
104 104 content_type_header = 'Content-Type'
105 105 headers = {
106 106 content_type_header: content_type
107 107 }
108 108 return Response(
109 109 body=raw_body,
110 110 content_type=content_type,
111 111 headerlist=[(k, v) for k, v in headers.items()]
112 112 )
113 113
114 114
115 115 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
116 116 """
117 117 Generate a Response object with a JSON-RPC error body
118 118 """
119 119 headers = headers or {}
120 120 content_type = 'application/json'
121 121 content_type_header = 'Content-Type'
122 122 if content_type_header not in headers:
123 123 headers[content_type_header] = content_type
124 124
125 125 err_dict = {'id': retid, 'result': None, 'error': message}
126 126 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
127 127
128 128 return Response(
129 129 body=raw_body,
130 130 status=code,
131 131 content_type=content_type,
132 132 headerlist=[(k, v) for k, v in headers.items()]
133 133 )
134 134
135 135
136 136 def exception_view(exc, request):
137 137 rpc_id = getattr(request, 'rpc_id', None)
138 138
139 139 if isinstance(exc, JSONRPCError):
140 fault_message = safe_str(exc.message)
140 fault_message = safe_str(exc)
141 141 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
142 142 elif isinstance(exc, JSONRPCValidationError):
143 143 colander_exc = exc.colander_exception
144 144 # TODO(marcink): think maybe of nicer way to serialize errors ?
145 145 fault_message = colander_exc.asdict()
146 146 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
147 147 elif isinstance(exc, JSONRPCForbidden):
148 148 fault_message = 'Access was denied to this resource.'
149 149 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
150 150 elif isinstance(exc, HTTPNotFound):
151 151 method = request.rpc_method
152 152 log.debug('json-rpc method `%s` not found in list of '
153 153 'api calls: %s, rpc_id:%s',
154 154 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
155 155
156 156 similar = 'none'
157 157 try:
158 158 similar_paterns = [f'*{x}*' for x in method.split('_')]
159 159 similar_found = find_methods(
160 160 request.registry.jsonrpc_methods, similar_paterns)
161 161 similar = ', '.join(similar_found.keys()) or similar
162 162 except Exception:
163 163 # make the whole above block safe
164 164 pass
165 165
166 166 fault_message = "No such method: {}. Similar methods: {}".format(
167 167 method, similar)
168 168 else:
169 169 fault_message = 'undefined error'
170 170 exc_info = exc.exc_info()
171 171 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
172 172
173 173 statsd = request.registry.statsd
174 174 if statsd:
175 175 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
176 176 statsd.incr('rhodecode_exception_total',
177 177 tags=["exc_source:api", f"type:{exc_type}"])
178 178
179 179 return jsonrpc_error(request, fault_message, rpc_id)
180 180
181 181
182 182 def request_view(request):
183 183 """
184 184 Main request handling method. It handles all logic to call a specific
185 185 exposed method
186 186 """
187 187 # cython compatible inspect
188 188 from rhodecode.config.patches import inspect_getargspec
189 189 inspect = inspect_getargspec()
190 190
191 191 # check if we can find this session using api_key, get_by_auth_token
192 192 # search not expired tokens only
193 193 try:
194 194 api_user = User.get_by_auth_token(request.rpc_api_key)
195 195
196 196 if api_user is None:
197 197 return jsonrpc_error(
198 198 request, retid=request.rpc_id, message='Invalid API KEY')
199 199
200 200 if not api_user.active:
201 201 return jsonrpc_error(
202 202 request, retid=request.rpc_id,
203 203 message='Request from this user not allowed')
204 204
205 205 # check if we are allowed to use this IP
206 206 auth_u = AuthUser(
207 207 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
208 208 if not auth_u.ip_allowed:
209 209 return jsonrpc_error(
210 210 request, retid=request.rpc_id,
211 211 message='Request from IP:{} not allowed'.format(
212 212 request.rpc_ip_addr))
213 213 else:
214 214 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
215 215
216 216 # register our auth-user
217 217 request.rpc_user = auth_u
218 218 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
219 219
220 220 # now check if token is valid for API
221 221 auth_token = request.rpc_api_key
222 222 token_match = api_user.authenticate_by_token(
223 223 auth_token, roles=[UserApiKeys.ROLE_API])
224 224 invalid_token = not token_match
225 225
226 226 log.debug('Checking if API KEY is valid with proper role')
227 227 if invalid_token:
228 228 return jsonrpc_error(
229 229 request, retid=request.rpc_id,
230 230 message='API KEY invalid or, has bad role for an API call')
231 231
232 232 except Exception:
233 233 log.exception('Error on API AUTH')
234 234 return jsonrpc_error(
235 235 request, retid=request.rpc_id, message='Invalid API KEY')
236 236
237 237 method = request.rpc_method
238 238 func = request.registry.jsonrpc_methods[method]
239 239
240 240 # now that we have a method, add request._req_params to
241 241 # self.kargs and dispatch control to WGIController
242 242
243 243 argspec = inspect.getargspec(func)
244 244 arglist = argspec[0]
245 245 defs = argspec[3] or []
246 246 defaults = [type(a) for a in defs]
247 247 default_empty = type(NotImplemented)
248 248
249 249 # kw arguments required by this method
250 250 func_kwargs = dict(itertools.zip_longest(
251 251 reversed(arglist), reversed(defaults), fillvalue=default_empty))
252 252
253 253 # This attribute will need to be first param of a method that uses
254 254 # api_key, which is translated to instance of user at that name
255 255 user_var = 'apiuser'
256 256 request_var = 'request'
257 257
258 258 for arg in [user_var, request_var]:
259 259 if arg not in arglist:
260 260 return jsonrpc_error(
261 261 request,
262 262 retid=request.rpc_id,
263 263 message='This method [%s] does not support '
264 264 'required parameter `%s`' % (func.__name__, arg))
265 265
266 266 # get our arglist and check if we provided them as args
267 267 for arg, default in func_kwargs.items():
268 268 if arg in [user_var, request_var]:
269 269 # user_var and request_var are pre-hardcoded parameters and we
270 270 # don't need to do any translation
271 271 continue
272 272
273 273 # skip the required param check if it's default value is
274 274 # NotImplementedType (default_empty)
275 275 if default == default_empty and arg not in request.rpc_params:
276 276 return jsonrpc_error(
277 277 request,
278 278 retid=request.rpc_id,
279 279 message=('Missing non optional `%s` arg in JSON DATA' % arg)
280 280 )
281 281
282 282 # sanitize extra passed arguments
283 283 for k in list(request.rpc_params.keys()):
284 284 if k not in func_kwargs:
285 285 del request.rpc_params[k]
286 286
287 287 call_params = request.rpc_params
288 288 call_params.update({
289 289 'request': request,
290 290 'apiuser': auth_u
291 291 })
292 292
293 293 # register some common functions for usage
294 294 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
295 295
296 296 statsd = request.registry.statsd
297 297
298 298 try:
299 299 ret_value = func(**call_params)
300 300 resp = jsonrpc_response(request, ret_value)
301 301 if statsd:
302 302 statsd.incr('rhodecode_api_call_success_total')
303 303 return resp
304 304 except JSONRPCBaseError:
305 305 raise
306 306 except Exception:
307 307 log.exception('Unhandled exception occurred on api call: %s', func)
308 308 exc_info = sys.exc_info()
309 309 exc_id, exc_type_name = store_exception(
310 310 id(exc_info), exc_info, prefix='rhodecode-api')
311 311 error_headers = {
312 312 'RhodeCode-Exception-Id': str(exc_id),
313 313 'RhodeCode-Exception-Type': str(exc_type_name)
314 314 }
315 315 err_resp = jsonrpc_error(
316 316 request, retid=request.rpc_id, message='Internal server error',
317 317 headers=error_headers)
318 318 if statsd:
319 319 statsd.incr('rhodecode_api_call_fail_total')
320 320 return err_resp
321 321
322 322
323 323 def setup_request(request):
324 324 """
325 325 Parse a JSON-RPC request body. It's used inside the predicates method
326 326 to validate and bootstrap requests for usage in rpc calls.
327 327
328 328 We need to raise JSONRPCError here if we want to return some errors back to
329 329 user.
330 330 """
331 331
332 332 log.debug('Executing setup request: %r', request)
333 333 request.rpc_ip_addr = get_ip_addr(request.environ)
334 334 # TODO(marcink): deprecate GET at some point
335 335 if request.method not in ['POST', 'GET']:
336 336 log.debug('unsupported request method "%s"', request.method)
337 337 raise JSONRPCError(
338 338 'unsupported request method "%s". Please use POST' % request.method)
339 339
340 340 if 'CONTENT_LENGTH' not in request.environ:
341 341 log.debug("No Content-Length")
342 342 raise JSONRPCError("Empty body, No Content-Length in request")
343 343
344 344 else:
345 345 length = request.environ['CONTENT_LENGTH']
346 346 log.debug('Content-Length: %s', length)
347 347
348 348 if length == 0:
349 349 log.debug("Content-Length is 0")
350 350 raise JSONRPCError("Content-Length is 0")
351 351
352 352 raw_body = request.body
353 353 log.debug("Loading JSON body now")
354 354 try:
355 355 json_body = ext_json.json.loads(raw_body)
356 356 except ValueError as e:
357 357 # catch JSON errors Here
358 358 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
359 359
360 360 request.rpc_id = json_body.get('id')
361 361 request.rpc_method = json_body.get('method')
362 362
363 363 # check required base parameters
364 364 try:
365 365 api_key = json_body.get('api_key')
366 366 if not api_key:
367 367 api_key = json_body.get('auth_token')
368 368
369 369 if not api_key:
370 370 raise KeyError('api_key or auth_token')
371 371
372 372 # TODO(marcink): support passing in token in request header
373 373
374 374 request.rpc_api_key = api_key
375 375 request.rpc_id = json_body['id']
376 376 request.rpc_method = json_body['method']
377 377 request.rpc_params = json_body['args'] \
378 378 if isinstance(json_body['args'], dict) else {}
379 379
380 380 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
381 381 except KeyError as e:
382 382 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
383 383
384 384 log.debug('setup complete, now handling method:%s rpcid:%s',
385 385 request.rpc_method, request.rpc_id, )
386 386
387 387
388 388 class RoutePredicate(object):
389 389 def __init__(self, val, config):
390 390 self.val = val
391 391
392 392 def text(self):
393 393 return f'jsonrpc route = {self.val}'
394 394
395 395 phash = text
396 396
397 397 def __call__(self, info, request):
398 398 if self.val:
399 399 # potentially setup and bootstrap our call
400 400 setup_request(request)
401 401
402 402 # Always return True so that even if it isn't a valid RPC it
403 403 # will fall through to the underlaying handlers like notfound_view
404 404 return True
405 405
406 406
407 407 class NotFoundPredicate(object):
408 408 def __init__(self, val, config):
409 409 self.val = val
410 410 self.methods = config.registry.jsonrpc_methods
411 411
412 412 def text(self):
413 413 return f'jsonrpc method not found = {self.val}'
414 414
415 415 phash = text
416 416
417 417 def __call__(self, info, request):
418 418 return hasattr(request, 'rpc_method')
419 419
420 420
421 421 class MethodPredicate(object):
422 422 def __init__(self, val, config):
423 423 self.method = val
424 424
425 425 def text(self):
426 426 return f'jsonrpc method = {self.method}'
427 427
428 428 phash = text
429 429
430 430 def __call__(self, context, request):
431 431 # we need to explicitly return False here, so pyramid doesn't try to
432 432 # execute our view directly. We need our main handler to execute things
433 433 return getattr(request, 'rpc_method') == self.method
434 434
435 435
436 436 def add_jsonrpc_method(config, view, **kwargs):
437 437 # pop the method name
438 438 method = kwargs.pop('method', None)
439 439
440 440 if method is None:
441 441 raise ConfigurationError(
442 442 'Cannot register a JSON-RPC method without specifying the "method"')
443 443
444 444 # we define custom predicate, to enable to detect conflicting methods,
445 445 # those predicates are kind of "translation" from the decorator variables
446 446 # to internal predicates names
447 447
448 448 kwargs['jsonrpc_method'] = method
449 449
450 450 # register our view into global view store for validation
451 451 config.registry.jsonrpc_methods[method] = view
452 452
453 453 # we're using our main request_view handler, here, so each method
454 454 # has a unified handler for itself
455 455 config.add_view(request_view, route_name='apiv2', **kwargs)
456 456
457 457
458 458 class jsonrpc_method(object):
459 459 """
460 460 decorator that works similar to @add_view_config decorator,
461 461 but tailored for our JSON RPC
462 462 """
463 463
464 464 venusian = venusian # for testing injection
465 465
466 466 def __init__(self, method=None, **kwargs):
467 467 self.method = method
468 468 self.kwargs = kwargs
469 469
470 470 def __call__(self, wrapped):
471 471 kwargs = self.kwargs.copy()
472 472 kwargs['method'] = self.method or wrapped.__name__
473 473 depth = kwargs.pop('_depth', 0)
474 474
475 475 def callback(context, name, ob):
476 476 config = context.config.with_package(info.module)
477 477 config.add_jsonrpc_method(view=ob, **kwargs)
478 478
479 479 info = venusian.attach(wrapped, callback, category='pyramid',
480 480 depth=depth + 1)
481 481 if info.scope == 'class':
482 482 # ensure that attr is set if decorating a class method
483 483 kwargs.setdefault('attr', wrapped.__name__)
484 484
485 485 kwargs['_info'] = info.codeinfo # fbo action_method
486 486 return wrapped
487 487
488 488
489 489 class jsonrpc_deprecated_method(object):
490 490 """
491 491 Marks method as deprecated, adds log.warning, and inject special key to
492 492 the request variable to mark method as deprecated.
493 493 Also injects special docstring that extract_docs will catch to mark
494 494 method as deprecated.
495 495
496 496 :param use_method: specify which method should be used instead of
497 497 the decorated one
498 498
499 499 Use like::
500 500
501 501 @jsonrpc_method()
502 502 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
503 503 def old_func(request, apiuser, arg1, arg2):
504 504 ...
505 505 """
506 506
507 507 def __init__(self, use_method, deprecated_at_version):
508 508 self.use_method = use_method
509 509 self.deprecated_at_version = deprecated_at_version
510 510 self.deprecated_msg = ''
511 511
512 512 def __call__(self, func):
513 513 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
514 514 method=self.use_method)
515 515
516 516 docstring = """\n
517 517 .. deprecated:: {version}
518 518
519 519 {deprecation_message}
520 520
521 521 {original_docstring}
522 522 """
523 523 func.__doc__ = docstring.format(
524 524 version=self.deprecated_at_version,
525 525 deprecation_message=self.deprecated_msg,
526 526 original_docstring=func.__doc__)
527 527 return decorator.decorator(self.__wrapper, func)
528 528
529 529 def __wrapper(self, func, *fargs, **fkwargs):
530 530 log.warning('DEPRECATED API CALL on function %s, please '
531 531 'use `%s` instead', func, self.use_method)
532 532 # alter function docstring to mark as deprecated, this is picked up
533 533 # via fabric file that generates API DOC.
534 534 result = func(*fargs, **fkwargs)
535 535
536 536 request = fargs[0]
537 537 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
538 538 return result
539 539
540 540
541 541 def add_api_methods(config):
542 542 from rhodecode.api.views import (
543 543 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
544 544 server_api, search_api, testing_api, user_api, user_group_api)
545 545
546 546 config.scan('rhodecode.api.views')
547 547
548 548
549 549 def includeme(config):
550 550 plugin_module = 'rhodecode.api'
551 551 plugin_settings = get_plugin_settings(
552 552 plugin_module, config.registry.settings)
553 553
554 554 if not hasattr(config.registry, 'jsonrpc_methods'):
555 555 config.registry.jsonrpc_methods = OrderedDict()
556 556
557 557 # match filter by given method only
558 558 config.add_view_predicate('jsonrpc_method', MethodPredicate)
559 559 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
560 560
561 561 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
562 562 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
563 563
564 564 config.add_route_predicate(
565 565 'jsonrpc_call', RoutePredicate)
566 566
567 567 config.add_route(
568 568 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
569 569
570 570 # register some exception handling view
571 571 config.add_view(exception_view, context=JSONRPCBaseError)
572 572 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
573 573
574 574 add_api_methods(config)
@@ -1,418 +1,418 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 logging
20 20 import itertools
21 21 import base64
22 22
23 23 from rhodecode.api import (
24 24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
25 25
26 26 from rhodecode.api.utils import (
27 27 Optional, OAttr, has_superadmin_permission, get_user_or_error)
28 28 from rhodecode.lib.utils import repo2db_mapper
29 29 from rhodecode.lib import system_info
30 30 from rhodecode.lib import user_sessions
31 31 from rhodecode.lib import exc_tracking
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.utils2 import safe_int
34 34 from rhodecode.model.db import UserIpMap
35 35 from rhodecode.model.scm import ScmModel
36 36 from rhodecode.model.settings import VcsSettingsModel
37 37 from rhodecode.apps.file_store import utils
38 38 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
39 39 FileOverSizeException
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 @jsonrpc_method()
45 45 def get_server_info(request, apiuser):
46 46 """
47 47 Returns the |RCE| server information.
48 48
49 49 This includes the running version of |RCE| and all installed
50 50 packages. This command takes the following options:
51 51
52 52 :param apiuser: This is filled automatically from the |authtoken|.
53 53 :type apiuser: AuthUser
54 54
55 55 Example output:
56 56
57 57 .. code-block:: bash
58 58
59 59 id : <id_given_in_input>
60 60 result : {
61 61 'modules': [<module name>,...]
62 62 'py_version': <python version>,
63 63 'platform': <platform type>,
64 64 'rhodecode_version': <rhodecode version>
65 65 }
66 66 error : null
67 67 """
68 68
69 69 if not has_superadmin_permission(apiuser):
70 70 raise JSONRPCForbidden()
71 71
72 72 server_info = ScmModel().get_server_info(request.environ)
73 73 # rhodecode-index requires those
74 74
75 75 server_info['index_storage'] = server_info['search']['value']['location']
76 76 server_info['storage'] = server_info['storage']['value']['path']
77 77
78 78 return server_info
79 79
80 80
81 81 @jsonrpc_method()
82 82 def get_repo_store(request, apiuser):
83 83 """
84 84 Returns the |RCE| repository storage information.
85 85
86 86 :param apiuser: This is filled automatically from the |authtoken|.
87 87 :type apiuser: AuthUser
88 88
89 89 Example output:
90 90
91 91 .. code-block:: bash
92 92
93 93 id : <id_given_in_input>
94 94 result : {
95 95 'modules': [<module name>,...]
96 96 'py_version': <python version>,
97 97 'platform': <platform type>,
98 98 'rhodecode_version': <rhodecode version>
99 99 }
100 100 error : null
101 101 """
102 102
103 103 if not has_superadmin_permission(apiuser):
104 104 raise JSONRPCForbidden()
105 105
106 106 path = VcsSettingsModel().get_repos_location()
107 107 return {"path": path}
108 108
109 109
110 110 @jsonrpc_method()
111 111 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
112 112 """
113 113 Displays the IP Address as seen from the |RCE| server.
114 114
115 115 * This command displays the IP Address, as well as all the defined IP
116 116 addresses for the specified user. If the ``userid`` is not set, the
117 117 data returned is for the user calling the method.
118 118
119 119 This command can only be run using an |authtoken| with admin rights to
120 120 the specified repository.
121 121
122 122 This command takes the following options:
123 123
124 124 :param apiuser: This is filled automatically from |authtoken|.
125 125 :type apiuser: AuthUser
126 126 :param userid: Sets the userid for which associated IP Address data
127 127 is returned.
128 128 :type userid: Optional(str or int)
129 129
130 130 Example output:
131 131
132 132 .. code-block:: bash
133 133
134 134 id : <id_given_in_input>
135 135 result : {
136 136 "server_ip_addr": "<ip_from_clien>",
137 137 "user_ips": [
138 138 {
139 139 "ip_addr": "<ip_with_mask>",
140 140 "ip_range": ["<start_ip>", "<end_ip>"],
141 141 },
142 142 ...
143 143 ]
144 144 }
145 145
146 146 """
147 147 if not has_superadmin_permission(apiuser):
148 148 raise JSONRPCForbidden()
149 149
150 150 userid = Optional.extract(userid, evaluate_locals=locals())
151 151 userid = getattr(userid, 'user_id', userid)
152 152
153 153 user = get_user_or_error(userid)
154 154 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
155 155 return {
156 156 'server_ip_addr': request.rpc_ip_addr,
157 157 'user_ips': ips
158 158 }
159 159
160 160
161 161 @jsonrpc_method()
162 162 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
163 163 """
164 164 Triggers a rescan of the specified repositories.
165 165
166 166 * If the ``remove_obsolete`` option is set, it also deletes repositories
167 167 that are found in the database but not on the file system, so called
168 168 "clean zombies".
169 169
170 170 This command can only be run using an |authtoken| with admin rights to
171 171 the specified repository.
172 172
173 173 This command takes the following options:
174 174
175 175 :param apiuser: This is filled automatically from the |authtoken|.
176 176 :type apiuser: AuthUser
177 177 :param remove_obsolete: Deletes repositories from the database that
178 178 are not found on the filesystem.
179 179 :type remove_obsolete: Optional(``True`` | ``False``)
180 180
181 181 Example output:
182 182
183 183 .. code-block:: bash
184 184
185 185 id : <id_given_in_input>
186 186 result : {
187 187 'added': [<added repository name>,...]
188 188 'removed': [<removed repository name>,...]
189 189 }
190 190 error : null
191 191
192 192 Example error output:
193 193
194 194 .. code-block:: bash
195 195
196 196 id : <id_given_in_input>
197 197 result : null
198 198 error : {
199 199 'Error occurred during rescan repositories action'
200 200 }
201 201
202 202 """
203 203 if not has_superadmin_permission(apiuser):
204 204 raise JSONRPCForbidden()
205 205
206 206 try:
207 207 rm_obsolete = Optional.extract(remove_obsolete)
208 208 added, removed = repo2db_mapper(ScmModel().repo_scan(),
209 209 remove_obsolete=rm_obsolete)
210 210 return {'added': added, 'removed': removed}
211 211 except Exception:
212 212 log.exception('Failed to run repo rescann')
213 213 raise JSONRPCError(
214 214 'Error occurred during rescan repositories action'
215 215 )
216 216
217 217
218 218 @jsonrpc_method()
219 219 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
220 220 """
221 221 Triggers a session cleanup action.
222 222
223 223 If the ``older_then`` option is set, only sessions that hasn't been
224 224 accessed in the given number of days will be removed.
225 225
226 226 This command can only be run using an |authtoken| with admin rights to
227 227 the specified repository.
228 228
229 229 This command takes the following options:
230 230
231 231 :param apiuser: This is filled automatically from the |authtoken|.
232 232 :type apiuser: AuthUser
233 233 :param older_then: Deletes session that hasn't been accessed
234 234 in given number of days.
235 235 :type older_then: Optional(int)
236 236
237 237 Example output:
238 238
239 239 .. code-block:: bash
240 240
241 241 id : <id_given_in_input>
242 242 result: {
243 243 "backend": "<type of backend>",
244 244 "sessions_removed": <number_of_removed_sessions>
245 245 }
246 246 error : null
247 247
248 248 Example error output:
249 249
250 250 .. code-block:: bash
251 251
252 252 id : <id_given_in_input>
253 253 result : null
254 254 error : {
255 255 'Error occurred during session cleanup'
256 256 }
257 257
258 258 """
259 259 if not has_superadmin_permission(apiuser):
260 260 raise JSONRPCForbidden()
261 261
262 262 older_then = safe_int(Optional.extract(older_then)) or 60
263 263 older_than_seconds = 60 * 60 * 24 * older_then
264 264
265 265 config = system_info.rhodecode_config().get_value()['value']['config']
266 266 session_model = user_sessions.get_session_handler(
267 267 config.get('beaker.session.type', 'memory'))(config)
268 268
269 269 backend = session_model.SESSION_TYPE
270 270 try:
271 271 cleaned = session_model.clean_sessions(
272 272 older_than_seconds=older_than_seconds)
273 273 return {'sessions_removed': cleaned, 'backend': backend}
274 274 except user_sessions.CleanupCommand as msg:
275 return {'cleanup_command': msg.message, 'backend': backend}
275 return {'cleanup_command': str(msg), 'backend': backend}
276 276 except Exception as e:
277 277 log.exception('Failed session cleanup')
278 278 raise JSONRPCError(
279 279 'Error occurred during session cleanup'
280 280 )
281 281
282 282
283 283 @jsonrpc_method()
284 284 def get_method(request, apiuser, pattern=Optional('*')):
285 285 """
286 286 Returns list of all available API methods. By default match pattern
287 287 os "*" but any other pattern can be specified. eg *comment* will return
288 288 all methods with comment inside them. If just single method is matched
289 289 returned data will also include method specification
290 290
291 291 This command can only be run using an |authtoken| with admin rights to
292 292 the specified repository.
293 293
294 294 This command takes the following options:
295 295
296 296 :param apiuser: This is filled automatically from the |authtoken|.
297 297 :type apiuser: AuthUser
298 298 :param pattern: pattern to match method names against
299 299 :type pattern: Optional("*")
300 300
301 301 Example output:
302 302
303 303 .. code-block:: bash
304 304
305 305 id : <id_given_in_input>
306 306 "result": [
307 307 "changeset_comment",
308 308 "comment_pull_request",
309 309 "comment_commit"
310 310 ]
311 311 error : null
312 312
313 313 .. code-block:: bash
314 314
315 315 id : <id_given_in_input>
316 316 "result": [
317 317 "comment_commit",
318 318 {
319 319 "apiuser": "<RequiredType>",
320 320 "comment_type": "<Optional:u'note'>",
321 321 "commit_id": "<RequiredType>",
322 322 "message": "<RequiredType>",
323 323 "repoid": "<RequiredType>",
324 324 "request": "<RequiredType>",
325 325 "resolves_comment_id": "<Optional:None>",
326 326 "status": "<Optional:None>",
327 327 "userid": "<Optional:<OptionalAttr:apiuser>>"
328 328 }
329 329 ]
330 330 error : null
331 331 """
332 332 from rhodecode.config.patches import inspect_getargspec
333 333 inspect = inspect_getargspec()
334 334
335 335 if not has_superadmin_permission(apiuser):
336 336 raise JSONRPCForbidden()
337 337
338 338 pattern = Optional.extract(pattern)
339 339
340 340 matches = find_methods(request.registry.jsonrpc_methods, pattern)
341 341
342 342 args_desc = []
343 343 matches_keys = list(matches.keys())
344 344 if len(matches_keys) == 1:
345 345 func = matches[matches_keys[0]]
346 346
347 347 argspec = inspect.getargspec(func)
348 348 arglist = argspec[0]
349 349 defaults = list(map(repr, argspec[3] or []))
350 350
351 351 default_empty = '<RequiredType>'
352 352
353 353 # kw arguments required by this method
354 354 func_kwargs = dict(itertools.zip_longest(
355 355 reversed(arglist), reversed(defaults), fillvalue=default_empty))
356 356 args_desc.append(func_kwargs)
357 357
358 358 return matches_keys + args_desc
359 359
360 360
361 361 @jsonrpc_method()
362 362 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
363 363 """
364 364 Stores sent exception inside the built-in exception tracker in |RCE| server.
365 365
366 366 This command can only be run using an |authtoken| with admin rights to
367 367 the specified repository.
368 368
369 369 This command takes the following options:
370 370
371 371 :param apiuser: This is filled automatically from the |authtoken|.
372 372 :type apiuser: AuthUser
373 373
374 374 :param exc_data_json: JSON data with exception e.g
375 375 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
376 376 :type exc_data_json: JSON data
377 377
378 378 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
379 379 :type prefix: Optional("rhodecode")
380 380
381 381 Example output:
382 382
383 383 .. code-block:: bash
384 384
385 385 id : <id_given_in_input>
386 386 "result": {
387 387 "exc_id": 139718459226384,
388 388 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
389 389 }
390 390 error : null
391 391 """
392 392 if not has_superadmin_permission(apiuser):
393 393 raise JSONRPCForbidden()
394 394
395 395 prefix = Optional.extract(prefix)
396 396 exc_id = exc_tracking.generate_id()
397 397
398 398 try:
399 399 exc_data = json.loads(exc_data_json)
400 400 except Exception:
401 401 log.error('Failed to parse JSON: %r', exc_data_json)
402 402 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
403 403 'Please make sure it contains a valid JSON.')
404 404
405 405 try:
406 406 exc_traceback = exc_data['exc_traceback']
407 407 exc_type_name = exc_data['exc_type_name']
408 408 except KeyError as err:
409 409 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
410 410 'in exc_data_json field. Missing: {}'.format(err))
411 411
412 412 exc_tracking._store_exception(
413 413 exc_id=exc_id, exc_traceback=exc_traceback,
414 414 exc_type_name=exc_type_name, prefix=prefix)
415 415
416 416 exc_url = request.route_url(
417 417 'admin_settings_exception_tracker_show', exception_id=exc_id)
418 418 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,93 +1,93 b''
1 1 # Copyright (C) 2016-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 logging
20 20
21 21
22 22 from pyramid.httpexceptions import HTTPFound
23 23
24 24 from rhodecode.apps._base import BaseAppView
25 25 from rhodecode.apps._base.navigation import navigation_list
26 26 from rhodecode.lib.auth import (
27 27 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
28 28 from rhodecode.lib.utils2 import safe_int
29 29 from rhodecode.lib import system_info
30 30 from rhodecode.lib import user_sessions
31 31 from rhodecode.lib import helpers as h
32 32
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class AdminSessionSettingsView(BaseAppView):
38 38
39 39 def load_default_context(self):
40 40 c = self._get_local_tmpl_context()
41 41 return c
42 42
43 43 @LoginRequired()
44 44 @HasPermissionAllDecorator('hg.admin')
45 45 def settings_sessions(self):
46 46 c = self.load_default_context()
47 47
48 48 c.active = 'sessions'
49 49 c.navlist = navigation_list(self.request)
50 50
51 51 c.cleanup_older_days = 60
52 52 older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days
53 53
54 54 config = system_info.rhodecode_config().get_value()['value']['config']
55 55 c.session_model = user_sessions.get_session_handler(
56 56 config.get('beaker.session.type', 'memory'))(config)
57 57
58 58 c.session_conf = c.session_model.config
59 59 c.session_count = c.session_model.get_count()
60 60 c.session_expired_count = c.session_model.get_expired_count(
61 61 older_than_seconds)
62 62
63 63 return self._get_template_context(c)
64 64
65 65 @LoginRequired()
66 66 @HasPermissionAllDecorator('hg.admin')
67 67 @CSRFRequired()
68 68 def settings_sessions_cleanup(self):
69 69 _ = self.request.translate
70 70 expire_days = safe_int(self.request.params.get('expire_days'))
71 71
72 72 if expire_days is None:
73 73 expire_days = 60
74 74
75 75 older_than_seconds = 60 * 60 * 24 * expire_days
76 76
77 77 config = system_info.rhodecode_config().get_value()['value']['config']
78 78 session_model = user_sessions.get_session_handler(
79 79 config.get('beaker.session.type', 'memory'))(config)
80 80
81 81 try:
82 82 session_model.clean_sessions(
83 83 older_than_seconds=older_than_seconds)
84 84 h.flash(_('Cleaned up old sessions'), category='success')
85 85 except user_sessions.CleanupCommand as msg:
86 h.flash(msg.message, category='warning')
86 h.flash(str(msg), category='warning')
87 87 except Exception as e:
88 88 log.exception('Failed session cleanup')
89 89 h.flash(_('Failed to cleanup up old sessions'), category='error')
90 90
91 91 redirect_to = self.request.resource_path(
92 92 self.context, route_name='admin_settings_sessions')
93 93 return HTTPFound(redirect_to)
General Comments 0
You need to be logged in to leave comments. Login now