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