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