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