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