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