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