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