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