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