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