##// END OF EJS Templates
api: fixes and changes to always return content type in API...
super-admin -
r5001:961992a2 default
parent child Browse files
Show More
@@ -1,578 +1,576 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 itertools
22 22 import logging
23 23 import sys
24 import types
25 24 import fnmatch
26 25
27 26 import decorator
27 import typing
28 28 import venusian
29 29 from collections import OrderedDict
30 30
31 31 from pyramid.exceptions import ConfigurationError
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34 from pyramid.httpexceptions import HTTPNotFound
35 35
36 36 from rhodecode.api.exc import (
37 37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 38 from rhodecode.apps._base import TemplateArgs
39 39 from rhodecode.lib.auth import AuthUser
40 40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
41 41 from rhodecode.lib.exc_tracking import store_exception
42 from rhodecode.lib.ext_json import json
42 from rhodecode.lib import ext_json
43 43 from rhodecode.lib.utils2 import safe_str
44 44 from rhodecode.lib.plugins.utils import get_plugin_settings
45 45 from rhodecode.model.db import User, UserApiKeys
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49 DEFAULT_RENDERER = 'jsonrpc_renderer'
50 50 DEFAULT_URL = '/_admin/apiv2'
51 51
52 52
53 53 def find_methods(jsonrpc_methods, pattern):
54 54 matches = OrderedDict()
55 55 if not isinstance(pattern, (list, tuple)):
56 56 pattern = [pattern]
57 57
58 58 for single_pattern in pattern:
59 59 for method_name, method in jsonrpc_methods.items():
60 60 if fnmatch.fnmatch(method_name, single_pattern):
61 61 matches[method_name] = method
62 62 return matches
63 63
64 64
65 65 class ExtJsonRenderer(object):
66 66 """
67 Custom renderer that mkaes use of our ext_json lib
67 Custom renderer that makes use of our ext_json lib
68 68
69 69 """
70 70
71 def __init__(self, serializer=json.dumps, **kw):
72 """ Any keyword arguments will be passed to the ``serializer``
73 function."""
74 self.serializer = serializer
75 self.kw = kw
71 def __init__(self):
72 self.serializer = ext_json.formatted_json
76 73
77 74 def __call__(self, info):
78 75 """ Returns a plain JSON-encoded string with content-type
79 76 ``application/json``. The content-type may be overridden by
80 77 setting ``request.response.content_type``."""
81 78
82 79 def _render(value, system):
83 80 request = system.get('request')
84 81 if request is not None:
85 82 response = request.response
86 83 ct = response.content_type
87 84 if ct == response.default_content_type:
88 85 response.content_type = 'application/json'
89 86
90 return self.serializer(value, **self.kw)
87 return self.serializer(value)
91 88
92 89 return _render
93 90
94 91
95 92 def jsonrpc_response(request, result):
96 93 rpc_id = getattr(request, 'rpc_id', None)
97 response = request.response
98
99 # store content_type before render is called
100 ct = response.content_type
101 94
102 95 ret_value = ''
103 96 if rpc_id:
104 ret_value = {
105 'id': rpc_id,
106 'result': result,
107 'error': None,
108 }
97 ret_value = {'id': rpc_id, 'result': result, 'error': None}
109 98
110 99 # fetch deprecation warnings, and store it inside results
111 100 deprecation = getattr(request, 'rpc_deprecation', None)
112 101 if deprecation:
113 102 ret_value['DEPRECATION_WARNING'] = deprecation
114 103
115 104 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
116 response.body = safe_str(raw_body, response.charset)
117
118 if ct == response.default_content_type:
119 response.content_type = 'application/json'
120
121 return response
105 content_type = 'application/json'
106 content_type_header = 'Content-Type'
107 headers = {
108 content_type_header: content_type
109 }
110 return Response(
111 body=raw_body,
112 content_type=content_type,
113 headerlist=[(k, v) for k, v in headers.items()]
114 )
122 115
123 116
124 def jsonrpc_error(request, message, retid=None, code=None, headers=None):
117 def jsonrpc_error(request, message, retid=None, code: typing.Optional[int] = None, headers: typing.Optional[dict] = None):
125 118 """
126 119 Generate a Response object with a JSON-RPC error body
120 """
121 headers = headers or {}
122 content_type = 'application/json'
123 content_type_header = 'Content-Type'
124 if content_type_header not in headers:
125 headers[content_type_header] = content_type
127 126
128 :param code:
129 :param retid:
130 :param message:
131 """
132 127 err_dict = {'id': retid, 'result': None, 'error': message}
133 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
128 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
134 129
135 130 return Response(
136 body=body,
131 body=raw_body,
137 132 status=code,
138 content_type='application/json',
139 headerlist=headers
133 content_type=content_type,
134 headerlist=[(k, v) for k, v in headers.items()]
140 135 )
141 136
142 137
143 138 def exception_view(exc, request):
144 139 rpc_id = getattr(request, 'rpc_id', None)
145 140
146 141 if isinstance(exc, JSONRPCError):
147 142 fault_message = safe_str(exc.message)
148 143 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
149 144 elif isinstance(exc, JSONRPCValidationError):
150 145 colander_exc = exc.colander_exception
151 146 # TODO(marcink): think maybe of nicer way to serialize errors ?
152 147 fault_message = colander_exc.asdict()
153 148 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
154 149 elif isinstance(exc, JSONRPCForbidden):
155 150 fault_message = 'Access was denied to this resource.'
156 151 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
157 152 elif isinstance(exc, HTTPNotFound):
158 153 method = request.rpc_method
159 154 log.debug('json-rpc method `%s` not found in list of '
160 155 'api calls: %s, rpc_id:%s',
161 method, request.registry.jsonrpc_methods.keys(), rpc_id)
156 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
162 157
163 158 similar = 'none'
164 159 try:
165 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
160 similar_paterns = [f'*{x}*' for x in method.split('_')]
166 161 similar_found = find_methods(
167 162 request.registry.jsonrpc_methods, similar_paterns)
168 163 similar = ', '.join(similar_found.keys()) or similar
169 164 except Exception:
170 165 # make the whole above block safe
171 166 pass
172 167
173 168 fault_message = "No such method: {}. Similar methods: {}".format(
174 169 method, similar)
175 170 else:
176 171 fault_message = 'undefined error'
177 172 exc_info = exc.exc_info()
178 173 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
179 174
180 175 statsd = request.registry.statsd
181 176 if statsd:
182 177 exc_type = "{}.{}".format(exc.__class__.__module__, exc.__class__.__name__)
183 178 statsd.incr('rhodecode_exception_total',
184 179 tags=["exc_source:api", "type:{}".format(exc_type)])
185 180
186 181 return jsonrpc_error(request, fault_message, rpc_id)
187 182
188 183
189 184 def request_view(request):
190 185 """
191 186 Main request handling method. It handles all logic to call a specific
192 187 exposed method
193 188 """
194 189 # cython compatible inspect
195 190 from rhodecode.config.patches import inspect_getargspec
196 191 inspect = inspect_getargspec()
197 192
198 193 # check if we can find this session using api_key, get_by_auth_token
199 194 # search not expired tokens only
200 195 try:
201 196 api_user = User.get_by_auth_token(request.rpc_api_key)
202 197
203 198 if api_user is None:
204 199 return jsonrpc_error(
205 200 request, retid=request.rpc_id, message='Invalid API KEY')
206 201
207 202 if not api_user.active:
208 203 return jsonrpc_error(
209 204 request, retid=request.rpc_id,
210 205 message='Request from this user not allowed')
211 206
212 207 # check if we are allowed to use this IP
213 208 auth_u = AuthUser(
214 209 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
215 210 if not auth_u.ip_allowed:
216 211 return jsonrpc_error(
217 212 request, retid=request.rpc_id,
218 213 message='Request from IP:%s not allowed' % (
219 214 request.rpc_ip_addr,))
220 215 else:
221 216 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
222 217
223 218 # register our auth-user
224 219 request.rpc_user = auth_u
225 request.environ['rc_auth_user_id'] = auth_u.user_id
220 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
226 221
227 222 # now check if token is valid for API
228 223 auth_token = request.rpc_api_key
229 224 token_match = api_user.authenticate_by_token(
230 225 auth_token, roles=[UserApiKeys.ROLE_API])
231 226 invalid_token = not token_match
232 227
233 228 log.debug('Checking if API KEY is valid with proper role')
234 229 if invalid_token:
235 230 return jsonrpc_error(
236 231 request, retid=request.rpc_id,
237 232 message='API KEY invalid or, has bad role for an API call')
238 233
239 234 except Exception:
240 235 log.exception('Error on API AUTH')
241 236 return jsonrpc_error(
242 237 request, retid=request.rpc_id, message='Invalid API KEY')
243 238
244 239 method = request.rpc_method
245 240 func = request.registry.jsonrpc_methods[method]
246 241
247 242 # now that we have a method, add request._req_params to
248 243 # self.kargs and dispatch control to WGIController
244
249 245 argspec = inspect.getargspec(func)
250 246 arglist = argspec[0]
251 defaults = map(type, argspec[3] or [])
252 default_empty = types.NotImplementedType
247 defs = argspec[3] or []
248 defaults = [type(a) for a in defs]
249 default_empty = type(NotImplemented)
253 250
254 251 # kw arguments required by this method
255 252 func_kwargs = dict(itertools.zip_longest(
256 253 reversed(arglist), reversed(defaults), fillvalue=default_empty))
257 254
258 255 # This attribute will need to be first param of a method that uses
259 256 # api_key, which is translated to instance of user at that name
260 257 user_var = 'apiuser'
261 258 request_var = 'request'
262 259
263 260 for arg in [user_var, request_var]:
264 261 if arg not in arglist:
265 262 return jsonrpc_error(
266 263 request,
267 264 retid=request.rpc_id,
268 265 message='This method [%s] does not support '
269 266 'required parameter `%s`' % (func.__name__, arg))
270 267
271 268 # get our arglist and check if we provided them as args
272 269 for arg, default in func_kwargs.items():
273 270 if arg in [user_var, request_var]:
274 271 # user_var and request_var are pre-hardcoded parameters and we
275 272 # don't need to do any translation
276 273 continue
277 274
278 275 # skip the required param check if it's default value is
279 276 # NotImplementedType (default_empty)
280 277 if default == default_empty and arg not in request.rpc_params:
281 278 return jsonrpc_error(
282 279 request,
283 280 retid=request.rpc_id,
284 281 message=('Missing non optional `%s` arg in JSON DATA' % arg)
285 282 )
286 283
287 284 # sanitize extra passed arguments
288 for k in request.rpc_params.keys()[:]:
285 for k in list(request.rpc_params.keys()):
289 286 if k not in func_kwargs:
290 287 del request.rpc_params[k]
291 288
292 289 call_params = request.rpc_params
293 290 call_params.update({
294 291 'request': request,
295 292 'apiuser': auth_u
296 293 })
297 294
298 295 # register some common functions for usage
299 296 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
300 297
301 298 statsd = request.registry.statsd
302 299
303 300 try:
304 301 ret_value = func(**call_params)
305 302 resp = jsonrpc_response(request, ret_value)
306 303 if statsd:
307 304 statsd.incr('rhodecode_api_call_success_total')
308 305 return resp
309 306 except JSONRPCBaseError:
310 307 raise
311 308 except Exception:
312 309 log.exception('Unhandled exception occurred on api call: %s', func)
313 310 exc_info = sys.exc_info()
314 311 exc_id, exc_type_name = store_exception(
315 312 id(exc_info), exc_info, prefix='rhodecode-api')
316 error_headers = [('RhodeCode-Exception-Id', str(exc_id)),
317 ('RhodeCode-Exception-Type', str(exc_type_name))]
313 error_headers = {
314 'RhodeCode-Exception-Id': str(exc_id),
315 'RhodeCode-Exception-Type': str(exc_type_name)
316 }
318 317 err_resp = jsonrpc_error(
319 318 request, retid=request.rpc_id, message='Internal server error',
320 319 headers=error_headers)
321 320 if statsd:
322 321 statsd.incr('rhodecode_api_call_fail_total')
323 322 return err_resp
324 323
325 324
326 325 def setup_request(request):
327 326 """
328 327 Parse a JSON-RPC request body. It's used inside the predicates method
329 328 to validate and bootstrap requests for usage in rpc calls.
330 329
331 330 We need to raise JSONRPCError here if we want to return some errors back to
332 331 user.
333 332 """
334 333
335 334 log.debug('Executing setup request: %r', request)
336 335 request.rpc_ip_addr = get_ip_addr(request.environ)
337 336 # TODO(marcink): deprecate GET at some point
338 337 if request.method not in ['POST', 'GET']:
339 338 log.debug('unsupported request method "%s"', request.method)
340 339 raise JSONRPCError(
341 340 'unsupported request method "%s". Please use POST' % request.method)
342 341
343 342 if 'CONTENT_LENGTH' not in request.environ:
344 343 log.debug("No Content-Length")
345 344 raise JSONRPCError("Empty body, No Content-Length in request")
346 345
347 346 else:
348 347 length = request.environ['CONTENT_LENGTH']
349 348 log.debug('Content-Length: %s', length)
350 349
351 350 if length == 0:
352 351 log.debug("Content-Length is 0")
353 352 raise JSONRPCError("Content-Length is 0")
354 353
355 354 raw_body = request.body
356 355 log.debug("Loading JSON body now")
357 356 try:
358 json_body = json.loads(raw_body)
357 json_body = ext_json.json.loads(raw_body)
359 358 except ValueError as e:
360 359 # catch JSON errors Here
361 360 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
362 361
363 362 request.rpc_id = json_body.get('id')
364 363 request.rpc_method = json_body.get('method')
365 364
366 365 # check required base parameters
367 366 try:
368 367 api_key = json_body.get('api_key')
369 368 if not api_key:
370 369 api_key = json_body.get('auth_token')
371 370
372 371 if not api_key:
373 372 raise KeyError('api_key or auth_token')
374 373
375 374 # TODO(marcink): support passing in token in request header
376 375
377 376 request.rpc_api_key = api_key
378 377 request.rpc_id = json_body['id']
379 378 request.rpc_method = json_body['method']
380 379 request.rpc_params = json_body['args'] \
381 380 if isinstance(json_body['args'], dict) else {}
382 381
383 382 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
384 383 except KeyError as e:
385 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
384 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
386 385
387 386 log.debug('setup complete, now handling method:%s rpcid:%s',
388 387 request.rpc_method, request.rpc_id, )
389 388
390 389
391 390 class RoutePredicate(object):
392 391 def __init__(self, val, config):
393 392 self.val = val
394 393
395 394 def text(self):
396 395 return 'jsonrpc route = %s' % self.val
397 396
398 397 phash = text
399 398
400 399 def __call__(self, info, request):
401 400 if self.val:
402 401 # potentially setup and bootstrap our call
403 402 setup_request(request)
404 403
405 404 # Always return True so that even if it isn't a valid RPC it
406 405 # will fall through to the underlaying handlers like notfound_view
407 406 return True
408 407
409 408
410 409 class NotFoundPredicate(object):
411 410 def __init__(self, val, config):
412 411 self.val = val
413 412 self.methods = config.registry.jsonrpc_methods
414 413
415 414 def text(self):
416 415 return 'jsonrpc method not found = {}.'.format(self.val)
417 416
418 417 phash = text
419 418
420 419 def __call__(self, info, request):
421 420 return hasattr(request, 'rpc_method')
422 421
423 422
424 423 class MethodPredicate(object):
425 424 def __init__(self, val, config):
426 425 self.method = val
427 426
428 427 def text(self):
429 428 return 'jsonrpc method = %s' % self.method
430 429
431 430 phash = text
432 431
433 432 def __call__(self, context, request):
434 433 # we need to explicitly return False here, so pyramid doesn't try to
435 434 # execute our view directly. We need our main handler to execute things
436 435 return getattr(request, 'rpc_method') == self.method
437 436
438 437
439 438 def add_jsonrpc_method(config, view, **kwargs):
440 439 # pop the method name
441 440 method = kwargs.pop('method', None)
442 441
443 442 if method is None:
444 443 raise ConfigurationError(
445 444 'Cannot register a JSON-RPC method without specifying the "method"')
446 445
447 446 # we define custom predicate, to enable to detect conflicting methods,
448 447 # those predicates are kind of "translation" from the decorator variables
449 448 # to internal predicates names
450 449
451 450 kwargs['jsonrpc_method'] = method
452 451
453 452 # register our view into global view store for validation
454 453 config.registry.jsonrpc_methods[method] = view
455 454
456 455 # we're using our main request_view handler, here, so each method
457 456 # has a unified handler for itself
458 457 config.add_view(request_view, route_name='apiv2', **kwargs)
459 458
460 459
461 460 class jsonrpc_method(object):
462 461 """
463 462 decorator that works similar to @add_view_config decorator,
464 463 but tailored for our JSON RPC
465 464 """
466 465
467 466 venusian = venusian # for testing injection
468 467
469 468 def __init__(self, method=None, **kwargs):
470 469 self.method = method
471 470 self.kwargs = kwargs
472 471
473 472 def __call__(self, wrapped):
474 473 kwargs = self.kwargs.copy()
475 474 kwargs['method'] = self.method or wrapped.__name__
476 475 depth = kwargs.pop('_depth', 0)
477 476
478 477 def callback(context, name, ob):
479 478 config = context.config.with_package(info.module)
480 479 config.add_jsonrpc_method(view=ob, **kwargs)
481 480
482 481 info = venusian.attach(wrapped, callback, category='pyramid',
483 482 depth=depth + 1)
484 483 if info.scope == 'class':
485 484 # ensure that attr is set if decorating a class method
486 485 kwargs.setdefault('attr', wrapped.__name__)
487 486
488 487 kwargs['_info'] = info.codeinfo # fbo action_method
489 488 return wrapped
490 489
491 490
492 491 class jsonrpc_deprecated_method(object):
493 492 """
494 493 Marks method as deprecated, adds log.warning, and inject special key to
495 494 the request variable to mark method as deprecated.
496 495 Also injects special docstring that extract_docs will catch to mark
497 496 method as deprecated.
498 497
499 498 :param use_method: specify which method should be used instead of
500 499 the decorated one
501 500
502 501 Use like::
503 502
504 503 @jsonrpc_method()
505 504 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
506 505 def old_func(request, apiuser, arg1, arg2):
507 506 ...
508 507 """
509 508
510 509 def __init__(self, use_method, deprecated_at_version):
511 510 self.use_method = use_method
512 511 self.deprecated_at_version = deprecated_at_version
513 512 self.deprecated_msg = ''
514 513
515 514 def __call__(self, func):
516 515 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
517 516 method=self.use_method)
518 517
519 518 docstring = """\n
520 519 .. deprecated:: {version}
521 520
522 521 {deprecation_message}
523 522
524 523 {original_docstring}
525 524 """
526 525 func.__doc__ = docstring.format(
527 526 version=self.deprecated_at_version,
528 527 deprecation_message=self.deprecated_msg,
529 528 original_docstring=func.__doc__)
530 529 return decorator.decorator(self.__wrapper, func)
531 530
532 531 def __wrapper(self, func, *fargs, **fkwargs):
533 532 log.warning('DEPRECATED API CALL on function %s, please '
534 533 'use `%s` instead', func, self.use_method)
535 534 # alter function docstring to mark as deprecated, this is picked up
536 535 # via fabric file that generates API DOC.
537 536 result = func(*fargs, **fkwargs)
538 537
539 538 request = fargs[0]
540 539 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
541 540 return result
542 541
543 542
544 543 def add_api_methods(config):
545 544 from rhodecode.api.views import (
546 545 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
547 546 server_api, search_api, testing_api, user_api, user_group_api)
548 547
549 548 config.scan('rhodecode.api.views')
550 549
551 550
552 551 def includeme(config):
553 552 plugin_module = 'rhodecode.api'
554 553 plugin_settings = get_plugin_settings(
555 554 plugin_module, config.registry.settings)
556 555
557 556 if not hasattr(config.registry, 'jsonrpc_methods'):
558 557 config.registry.jsonrpc_methods = OrderedDict()
559 558
560 559 # match filter by given method only
561 560 config.add_view_predicate('jsonrpc_method', MethodPredicate)
562 561 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
563 562
564 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
565 serializer=json.dumps, indent=4))
563 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
566 564 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
567 565
568 566 config.add_route_predicate(
569 567 'jsonrpc_call', RoutePredicate)
570 568
571 569 config.add_route(
572 570 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
573 571
574 572 # register some exception handling view
575 573 config.add_view(exception_view, context=JSONRPCBaseError)
576 574 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
577 575
578 576 add_api_methods(config)
@@ -1,134 +1,134 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 params = params.replace('"args": {}', '"args": null')
66 params = params.replace(b'"args": {}', b'"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 params = params.replace('"args": {}', '"args": 1')
74 params = params.replace(b'"args": {}', b'"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 83 expected = 'No such method: not_existing. Similar methods: none'
84 84 assert_error(id_, expected, given=response.body)
85 85
86 86 def test_api_non_existing_method_have_similar(self, request):
87 87 id_, params = build_data(self.apikey, 'comment', args='xx')
88 88 response = api_call(self.app, params)
89 89 expected = 'No such method: comment. ' \
90 90 'Similar methods: changeset_comment, comment_pull_request, ' \
91 91 'get_pull_request_comments, comment_commit, edit_comment, ' \
92 92 'get_comment, get_repo_comments'
93 93 assert_error(id_, expected, given=response.body)
94 94
95 95 def test_api_disabled_user(self, request):
96 96
97 97 def set_active(active):
98 98 from rhodecode.model.db import Session, User
99 99 user = User.get_by_auth_token(self.apikey)
100 100 user.active = active
101 101 Session().add(user)
102 102 Session().commit()
103 103
104 104 request.addfinalizer(lambda: set_active(True))
105 105
106 106 set_active(False)
107 107 id_, params = build_data(self.apikey, 'test', args='xx')
108 108 response = api_call(self.app, params)
109 109 expected = 'Request from this user not allowed'
110 110 assert_error(id_, expected, given=response.body)
111 111
112 112 def test_api_args_is_null(self):
113 113 __, params = build_data(self.apikey, 'get_users', )
114 params = params.replace('"args": {}', '"args": null')
114 params = params.replace(b'"args": {}', b'"args": null')
115 115 response = api_call(self.app, params)
116 116 assert response.status == '200 OK'
117 117
118 118 def test_api_args_is_bad(self):
119 119 __, params = build_data(self.apikey, 'get_users', )
120 params = params.replace('"args": {}', '"args": 1')
120 params = params.replace(b'"args": {}', b'"args": 1')
121 121 response = api_call(self.app, params)
122 122 assert response.status == '200 OK'
123 123
124 124 def test_api_args_different_args(self):
125 125 import string
126 126 expected = {
127 127 'ascii_letters': string.ascii_letters,
128 128 'ws': string.whitespace,
129 129 'printables': string.printable
130 130 }
131 131 id_, params = build_data(self.apikey, 'test', args=expected)
132 132 response = api_call(self.app, params)
133 133 assert response.status == '200 OK'
134 134 assert_ok(id_, expected, response.body)
@@ -1,123 +1,124 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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
22 22 import random
23 23 import pytest
24 24
25 25 from rhodecode.api.utils import get_origin
26 26 from rhodecode.lib.ext_json import json
27 27
28 28
29 29 def jsonify(obj):
30 30 return json.loads(json.dumps(obj))
31 31
32 32
33 33 API_URL = '/_admin/api'
34 34
35 35
36 36 def assert_call_ok(id_, given):
37 37 expected = jsonify({
38 38 'id': id_,
39 39 'error': None,
40 40 'result': None
41 41 })
42 42 given = json.loads(given)
43 43
44 44 assert expected['id'] == given['id']
45 45 assert expected['error'] == given['error']
46 46 return given['result']
47 47
48 48
49 49 def assert_ok(id_, expected, given):
50 50 given = json.loads(given)
51 51 if given.get('error'):
52 52 err = given['error']
53 53 pytest.fail(u"Unexpected ERROR in success response: {}".format(err))
54 54
55 55 expected = jsonify({
56 56 'id': id_,
57 57 'error': None,
58 58 'result': expected
59 59 })
60 60
61 61 assert expected == given
62 62
63 63
64 64 def assert_error(id_, expected, given):
65 65 expected = jsonify({
66 66 'id': id_,
67 67 'error': expected,
68 68 'result': None
69 69 })
70 70 given = json.loads(given)
71 71 assert expected == given
72 72
73 73
74 74 def build_data(apikey, method, **kw):
75 75 """
76 76 Builds API data with given random ID
77 77 """
78 78 random_id = random.randrange(1, 9999)
79 79 return random_id, json.dumps({
80 80 "id": random_id,
81 81 "api_key": apikey,
82 82 "method": method,
83 83 "args": kw
84 84 })
85 85
86 86
87 87 def api_call(app, params, status=None):
88 88 response = app.post(
89 API_URL, content_type='application/json', params=params, status=status)
89 API_URL, content_type='application/json', params=params, status=status,
90 headers=[('Content-Type', 'application/json')])
90 91 return response
91 92
92 93
93 94 def crash(*args, **kwargs):
94 95 raise Exception('Total Crash !')
95 96
96 97
97 98 def expected_permissions(object_with_permissions):
98 99 """
99 100 Returns the expected permissions structure for the given object.
100 101
101 102 The object is expected to be a `Repository`, `RepositoryGroup`,
102 103 or `UserGroup`. They all implement the same permission handling
103 104 API.
104 105 """
105 106 permissions = []
106 107 for _user in object_with_permissions.permissions():
107 108 user_data = {
108 109 'name': _user.username,
109 110 'permission': _user.permission,
110 111 'origin': get_origin(_user),
111 112 'type': "user",
112 113 }
113 114 permissions.append(user_data)
114 115
115 116 for _user_group in object_with_permissions.permission_user_groups():
116 117 user_group_data = {
117 118 'name': _user_group.users_group_name,
118 119 'permission': _user_group.permission,
119 120 'origin': get_origin(_user_group),
120 121 'type': "user_group",
121 122 }
122 123 permissions.append(user_group_data)
123 124 return permissions
@@ -1,2524 +1,2527 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 logging
22 22 import time
23 23
24 24 import rhodecode
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib import audit_logger, rc_cache, channelstream
33 33 from rhodecode.lib import repo_maintenance
34 34 from rhodecode.lib.auth import (
35 35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
36 36 HasRepoPermissionAnyApi)
37 37 from rhodecode.lib.celerylib.utils import get_task_id
38 38 from rhodecode.lib.utils2 import (
39 39 str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.exceptions import (
42 42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
43 43 from rhodecode.lib.vcs import RepositoryError
44 44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (
48 48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
49 49 ChangesetComment)
50 50 from rhodecode.model.permission import PermissionModel
51 51 from rhodecode.model.pull_request import PullRequestModel
52 52 from rhodecode.model.repo import RepoModel
53 53 from rhodecode.model.scm import ScmModel, RepoList
54 54 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
55 55 from rhodecode.model import validation_schema
56 56 from rhodecode.model.validation_schema.schemas import repo_schema
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 @jsonrpc_method()
62 62 def get_repo(request, apiuser, repoid, cache=Optional(True)):
63 63 """
64 64 Gets an existing repository by its name or repository_id.
65 65
66 66 The members section so the output returns users groups or users
67 67 associated with that repository.
68 68
69 69 This command can only be run using an |authtoken| with admin rights,
70 70 or users with at least read rights to the |repo|.
71 71
72 72 :param apiuser: This is filled automatically from the |authtoken|.
73 73 :type apiuser: AuthUser
74 74 :param repoid: The repository name or repository id.
75 75 :type repoid: str or int
76 76 :param cache: use the cached value for last changeset
77 77 :type: cache: Optional(bool)
78 78
79 79 Example output:
80 80
81 81 .. code-block:: bash
82 82
83 83 {
84 84 "error": null,
85 85 "id": <repo_id>,
86 86 "result": {
87 87 "clone_uri": null,
88 88 "created_on": "timestamp",
89 89 "description": "repo description",
90 90 "enable_downloads": false,
91 91 "enable_locking": false,
92 92 "enable_statistics": false,
93 93 "followers": [
94 94 {
95 95 "active": true,
96 96 "admin": false,
97 97 "api_key": "****************************************",
98 98 "api_keys": [
99 99 "****************************************"
100 100 ],
101 101 "email": "user@example.com",
102 102 "emails": [
103 103 "user@example.com"
104 104 ],
105 105 "extern_name": "rhodecode",
106 106 "extern_type": "rhodecode",
107 107 "firstname": "username",
108 108 "ip_addresses": [],
109 109 "language": null,
110 110 "last_login": "2015-09-16T17:16:35.854",
111 111 "lastname": "surname",
112 112 "user_id": <user_id>,
113 113 "username": "name"
114 114 }
115 115 ],
116 116 "fork_of": "parent-repo",
117 117 "landing_rev": [
118 118 "rev",
119 119 "tip"
120 120 ],
121 121 "last_changeset": {
122 122 "author": "User <user@example.com>",
123 123 "branch": "default",
124 124 "date": "timestamp",
125 125 "message": "last commit message",
126 126 "parents": [
127 127 {
128 128 "raw_id": "commit-id"
129 129 }
130 130 ],
131 131 "raw_id": "commit-id",
132 132 "revision": <revision number>,
133 133 "short_id": "short id"
134 134 },
135 135 "lock_reason": null,
136 136 "locked_by": null,
137 137 "locked_date": null,
138 138 "owner": "owner-name",
139 139 "permissions": [
140 140 {
141 141 "name": "super-admin-name",
142 142 "origin": "super-admin",
143 143 "permission": "repository.admin",
144 144 "type": "user"
145 145 },
146 146 {
147 147 "name": "owner-name",
148 148 "origin": "owner",
149 149 "permission": "repository.admin",
150 150 "type": "user"
151 151 },
152 152 {
153 153 "name": "user-group-name",
154 154 "origin": "permission",
155 155 "permission": "repository.write",
156 156 "type": "user_group"
157 157 }
158 158 ],
159 159 "private": true,
160 160 "repo_id": 676,
161 161 "repo_name": "user-group/repo-name",
162 162 "repo_type": "hg"
163 163 }
164 164 }
165 165 """
166 166
167 167 repo = get_repo_or_error(repoid)
168 168 cache = Optional.extract(cache)
169 169
170 170 include_secrets = False
171 171 if has_superadmin_permission(apiuser):
172 172 include_secrets = True
173 173 else:
174 174 # check if we have at least read permission for this repo !
175 175 _perms = (
176 176 'repository.admin', 'repository.write', 'repository.read',)
177 177 validate_repo_permissions(apiuser, repoid, repo, _perms)
178 178
179 179 permissions = []
180 180 for _user in repo.permissions():
181 181 user_data = {
182 182 'name': _user.username,
183 183 'permission': _user.permission,
184 184 'origin': get_origin(_user),
185 185 'type': "user",
186 186 }
187 187 permissions.append(user_data)
188 188
189 189 for _user_group in repo.permission_user_groups():
190 190 user_group_data = {
191 191 'name': _user_group.users_group_name,
192 192 'permission': _user_group.permission,
193 193 'origin': get_origin(_user_group),
194 194 'type': "user_group",
195 195 }
196 196 permissions.append(user_group_data)
197 197
198 198 following_users = [
199 199 user.user.get_api_data(include_secrets=include_secrets)
200 200 for user in repo.followers]
201 201
202 202 if not cache:
203 203 repo.update_commit_cache()
204 204 data = repo.get_api_data(include_secrets=include_secrets)
205 205 data['permissions'] = permissions
206 206 data['followers'] = following_users
207 207 return data
208 208
209 209
210 210 @jsonrpc_method()
211 211 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
212 212 """
213 213 Lists all existing repositories.
214 214
215 215 This command can only be run using an |authtoken| with admin rights,
216 216 or users with at least read rights to |repos|.
217 217
218 218 :param apiuser: This is filled automatically from the |authtoken|.
219 219 :type apiuser: AuthUser
220 220 :param root: specify root repository group to fetch repositories.
221 221 filters the returned repositories to be members of given root group.
222 222 :type root: Optional(None)
223 223 :param traverse: traverse given root into subrepositories. With this flag
224 224 set to False, it will only return top-level repositories from `root`.
225 225 if root is empty it will return just top-level repositories.
226 226 :type traverse: Optional(True)
227 227
228 228
229 229 Example output:
230 230
231 231 .. code-block:: bash
232 232
233 233 id : <id_given_in_input>
234 234 result: [
235 235 {
236 236 "repo_id" : "<repo_id>",
237 237 "repo_name" : "<reponame>"
238 238 "repo_type" : "<repo_type>",
239 239 "clone_uri" : "<clone_uri>",
240 240 "private": : "<bool>",
241 241 "created_on" : "<datetimecreated>",
242 242 "description" : "<description>",
243 243 "landing_rev": "<landing_rev>",
244 244 "owner": "<repo_owner>",
245 245 "fork_of": "<name_of_fork_parent>",
246 246 "enable_downloads": "<bool>",
247 247 "enable_locking": "<bool>",
248 248 "enable_statistics": "<bool>",
249 249 },
250 250 ...
251 251 ]
252 252 error: null
253 253 """
254 254
255 255 include_secrets = has_superadmin_permission(apiuser)
256 256 _perms = ('repository.read', 'repository.write', 'repository.admin',)
257 257 extras = {'user': apiuser}
258 258
259 259 root = Optional.extract(root)
260 260 traverse = Optional.extract(traverse, binary=True)
261 261
262 262 if root:
263 263 # verify parent existance, if it's empty return an error
264 264 parent = RepoGroup.get_by_group_name(root)
265 265 if not parent:
266 266 raise JSONRPCError(
267 267 'Root repository group `{}` does not exist'.format(root))
268 268
269 269 if traverse:
270 270 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
271 271 else:
272 272 repos = RepoModel().get_repos_for_root(root=parent)
273 273 else:
274 274 if traverse:
275 275 repos = RepoModel().get_all()
276 276 else:
277 277 # return just top-level
278 278 repos = RepoModel().get_repos_for_root(root=None)
279 279
280 280 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
281 281 return [repo.get_api_data(include_secrets=include_secrets)
282 282 for repo in repo_list]
283 283
284 284
285 285 @jsonrpc_method()
286 286 def get_repo_changeset(request, apiuser, repoid, revision,
287 287 details=Optional('basic')):
288 288 """
289 289 Returns information about a changeset.
290 290
291 291 Additionally parameters define the amount of details returned by
292 292 this function.
293 293
294 294 This command can only be run using an |authtoken| with admin rights,
295 295 or users with at least read rights to the |repo|.
296 296
297 297 :param apiuser: This is filled automatically from the |authtoken|.
298 298 :type apiuser: AuthUser
299 299 :param repoid: The repository name or repository id
300 300 :type repoid: str or int
301 301 :param revision: revision for which listing should be done
302 302 :type revision: str
303 303 :param details: details can be 'basic|extended|full' full gives diff
304 304 info details like the diff itself, and number of changed files etc.
305 305 :type details: Optional(str)
306 306
307 307 """
308 308 repo = get_repo_or_error(repoid)
309 309 if not has_superadmin_permission(apiuser):
310 310 _perms = ('repository.admin', 'repository.write', 'repository.read',)
311 311 validate_repo_permissions(apiuser, repoid, repo, _perms)
312 312
313 313 changes_details = Optional.extract(details)
314 314 _changes_details_types = ['basic', 'extended', 'full']
315 315 if changes_details not in _changes_details_types:
316 316 raise JSONRPCError(
317 317 'ret_type must be one of %s' % (
318 318 ','.join(_changes_details_types)))
319 319
320 320 vcs_repo = repo.scm_instance()
321 321 pre_load = ['author', 'branch', 'date', 'message', 'parents',
322 322 'status', '_commit', '_file_paths']
323 323
324 324 try:
325 325 commit = repo.get_commit(commit_id=revision, pre_load=pre_load)
326 326 except TypeError as e:
327 327 raise JSONRPCError(safe_str(e))
328 328 _cs_json = commit.__json__()
329 329 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
330 330 if changes_details == 'full':
331 331 _cs_json['refs'] = commit._get_refs()
332 332 return _cs_json
333 333
334 334
335 335 @jsonrpc_method()
336 336 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
337 337 details=Optional('basic')):
338 338 """
339 339 Returns a set of commits limited by the number starting
340 340 from the `start_rev` option.
341 341
342 342 Additional parameters define the amount of details returned by this
343 343 function.
344 344
345 345 This command can only be run using an |authtoken| with admin rights,
346 346 or users with at least read rights to |repos|.
347 347
348 348 :param apiuser: This is filled automatically from the |authtoken|.
349 349 :type apiuser: AuthUser
350 350 :param repoid: The repository name or repository ID.
351 351 :type repoid: str or int
352 352 :param start_rev: The starting revision from where to get changesets.
353 353 :type start_rev: str
354 354 :param limit: Limit the number of commits to this amount
355 355 :type limit: str or int
356 356 :param details: Set the level of detail returned. Valid option are:
357 357 ``basic``, ``extended`` and ``full``.
358 358 :type details: Optional(str)
359 359
360 360 .. note::
361 361
362 362 Setting the parameter `details` to the value ``full`` is extensive
363 363 and returns details like the diff itself, and the number
364 364 of changed files.
365 365
366 366 """
367 367 repo = get_repo_or_error(repoid)
368 368 if not has_superadmin_permission(apiuser):
369 369 _perms = ('repository.admin', 'repository.write', 'repository.read',)
370 370 validate_repo_permissions(apiuser, repoid, repo, _perms)
371 371
372 372 changes_details = Optional.extract(details)
373 373 _changes_details_types = ['basic', 'extended', 'full']
374 374 if changes_details not in _changes_details_types:
375 375 raise JSONRPCError(
376 376 'ret_type must be one of %s' % (
377 377 ','.join(_changes_details_types)))
378 378
379 379 limit = int(limit)
380 380 pre_load = ['author', 'branch', 'date', 'message', 'parents',
381 381 'status', '_commit', '_file_paths']
382 382
383 383 vcs_repo = repo.scm_instance()
384 384 # SVN needs a special case to distinguish its index and commit id
385 385 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
386 386 start_rev = vcs_repo.commit_ids[0]
387 387
388 388 try:
389 389 commits = vcs_repo.get_commits(
390 390 start_id=start_rev, pre_load=pre_load, translate_tags=False)
391 391 except TypeError as e:
392 392 raise JSONRPCError(safe_str(e))
393 393 except Exception:
394 394 log.exception('Fetching of commits failed')
395 395 raise JSONRPCError('Error occurred during commit fetching')
396 396
397 397 ret = []
398 398 for cnt, commit in enumerate(commits):
399 399 if cnt >= limit != -1:
400 400 break
401 401 _cs_json = commit.__json__()
402 402 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
403 403 if changes_details == 'full':
404 404 _cs_json['refs'] = {
405 405 'branches': [commit.branch],
406 406 'bookmarks': getattr(commit, 'bookmarks', []),
407 407 'tags': commit.tags
408 408 }
409 409 ret.append(_cs_json)
410 410 return ret
411 411
412 412
413 413 @jsonrpc_method()
414 414 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
415 415 ret_type=Optional('all'), details=Optional('basic'),
416 416 max_file_bytes=Optional(None)):
417 417 """
418 418 Returns a list of nodes and children in a flat list for a given
419 419 path at given revision.
420 420
421 421 It's possible to specify ret_type to show only `files` or `dirs`.
422 422
423 423 This command can only be run using an |authtoken| with admin rights,
424 424 or users with at least read rights to |repos|.
425 425
426 426 :param apiuser: This is filled automatically from the |authtoken|.
427 427 :type apiuser: AuthUser
428 428 :param repoid: The repository name or repository ID.
429 429 :type repoid: str or int
430 430 :param revision: The revision for which listing should be done.
431 431 :type revision: str
432 432 :param root_path: The path from which to start displaying.
433 433 :type root_path: str
434 434 :param ret_type: Set the return type. Valid options are
435 435 ``all`` (default), ``files`` and ``dirs``.
436 436 :type ret_type: Optional(str)
437 437 :param details: Returns extended information about nodes, such as
438 438 md5, binary, and or content.
439 439 The valid options are ``basic`` and ``full``.
440 440 :type details: Optional(str)
441 441 :param max_file_bytes: Only return file content under this file size bytes
442 442 :type details: Optional(int)
443 443
444 444 Example output:
445 445
446 446 .. code-block:: bash
447 447
448 448 id : <id_given_in_input>
449 449 result: [
450 450 {
451 451 "binary": false,
452 452 "content": "File line",
453 453 "extension": "md",
454 454 "lines": 2,
455 455 "md5": "059fa5d29b19c0657e384749480f6422",
456 456 "mimetype": "text/x-minidsrc",
457 457 "name": "file.md",
458 458 "size": 580,
459 459 "type": "file"
460 460 },
461 461 ...
462 462 ]
463 463 error: null
464 464 """
465 465
466 466 repo = get_repo_or_error(repoid)
467 467 if not has_superadmin_permission(apiuser):
468 468 _perms = ('repository.admin', 'repository.write', 'repository.read',)
469 469 validate_repo_permissions(apiuser, repoid, repo, _perms)
470 470
471 471 ret_type = Optional.extract(ret_type)
472 472 details = Optional.extract(details)
473 max_file_bytes = Optional.extract(max_file_bytes)
474
473 475 _extended_types = ['basic', 'full']
474 476 if details not in _extended_types:
475 477 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
476 478 extended_info = False
477 479 content = False
478 480 if details == 'basic':
479 481 extended_info = True
480 482
481 483 if details == 'full':
482 484 extended_info = content = True
483 485
484 486 _map = {}
485 487 try:
486 488 # check if repo is not empty by any chance, skip quicker if it is.
487 489 _scm = repo.scm_instance()
488 490 if _scm.is_empty():
489 491 return []
490 492
491 493 _d, _f = ScmModel().get_nodes(
492 494 repo, revision, root_path, flat=False,
493 495 extended_info=extended_info, content=content,
494 496 max_file_bytes=max_file_bytes)
497
495 498 _map = {
496 499 'all': _d + _f,
497 500 'files': _f,
498 501 'dirs': _d,
499 502 }
500 503 return _map[ret_type]
501 504 except KeyError:
502 505 raise JSONRPCError(
503 506 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
504 507 except Exception:
505 508 log.exception("Exception occurred while trying to get repo nodes")
506 509 raise JSONRPCError(
507 510 'failed to get repo: `%s` nodes' % repo.repo_name
508 511 )
509 512
510 513
511 514 @jsonrpc_method()
512 515 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
513 516 max_file_bytes=Optional(None), details=Optional('basic'),
514 517 cache=Optional(True)):
515 518 """
516 519 Returns a single file from repository at given revision.
517 520
518 521 This command can only be run using an |authtoken| with admin rights,
519 522 or users with at least read rights to |repos|.
520 523
521 524 :param apiuser: This is filled automatically from the |authtoken|.
522 525 :type apiuser: AuthUser
523 526 :param repoid: The repository name or repository ID.
524 527 :type repoid: str or int
525 528 :param commit_id: The revision for which listing should be done.
526 529 :type commit_id: str
527 530 :param file_path: The path from which to start displaying.
528 531 :type file_path: str
529 532 :param details: Returns different set of information about nodes.
530 533 The valid options are ``minimal`` ``basic`` and ``full``.
531 534 :type details: Optional(str)
532 535 :param max_file_bytes: Only return file content under this file size bytes
533 536 :type max_file_bytes: Optional(int)
534 537 :param cache: Use internal caches for fetching files. If disabled fetching
535 538 files is slower but more memory efficient
536 539 :type cache: Optional(bool)
537 540
538 541 Example output:
539 542
540 543 .. code-block:: bash
541 544
542 545 id : <id_given_in_input>
543 546 result: {
544 547 "binary": false,
545 548 "extension": "py",
546 549 "lines": 35,
547 550 "content": "....",
548 551 "md5": "76318336366b0f17ee249e11b0c99c41",
549 552 "mimetype": "text/x-python",
550 553 "name": "python.py",
551 554 "size": 817,
552 555 "type": "file",
553 556 }
554 557 error: null
555 558 """
556 559
557 560 repo = get_repo_or_error(repoid)
558 561 if not has_superadmin_permission(apiuser):
559 562 _perms = ('repository.admin', 'repository.write', 'repository.read',)
560 563 validate_repo_permissions(apiuser, repoid, repo, _perms)
561 564
562 565 cache = Optional.extract(cache, binary=True)
563 566 details = Optional.extract(details)
564 567 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
565 568 if details not in _extended_types:
566 569 raise JSONRPCError(
567 570 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
568 571 extended_info = False
569 572 content = False
570 573
571 574 if details == 'minimal':
572 575 extended_info = False
573 576
574 577 elif details == 'basic':
575 578 extended_info = True
576 579
577 580 elif details == 'full':
578 581 extended_info = content = True
579 582
580 583 file_path = safe_unicode(file_path)
581 584 try:
582 585 # check if repo is not empty by any chance, skip quicker if it is.
583 586 _scm = repo.scm_instance()
584 587 if _scm.is_empty():
585 588 return None
586 589
587 590 node = ScmModel().get_node(
588 591 repo, commit_id, file_path, extended_info=extended_info,
589 592 content=content, max_file_bytes=max_file_bytes, cache=cache)
590 593 except NodeDoesNotExistError:
591 594 raise JSONRPCError(u'There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
592 595 repo.repo_name, file_path, commit_id))
593 596 except Exception:
594 597 log.exception(u"Exception occurred while trying to get repo %s file",
595 598 repo.repo_name)
596 599 raise JSONRPCError(u'failed to get repo: `{}` file at path {}'.format(
597 600 repo.repo_name, file_path))
598 601
599 602 return node
600 603
601 604
602 605 @jsonrpc_method()
603 606 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
604 607 """
605 608 Returns a list of tree nodes for path at given revision. This api is built
606 609 strictly for usage in full text search building, and shouldn't be consumed
607 610
608 611 This command can only be run using an |authtoken| with admin rights,
609 612 or users with at least read rights to |repos|.
610 613
611 614 """
612 615
613 616 repo = get_repo_or_error(repoid)
614 617 if not has_superadmin_permission(apiuser):
615 618 _perms = ('repository.admin', 'repository.write', 'repository.read',)
616 619 validate_repo_permissions(apiuser, repoid, repo, _perms)
617 620
618 621 repo_id = repo.repo_id
619 622 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
620 623 cache_on = cache_seconds > 0
621 624
622 625 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
623 626 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
624 627
625 628 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
626 629 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
627 630
628 631 try:
629 632 # check if repo is not empty by any chance, skip quicker if it is.
630 633 _scm = repo.scm_instance()
631 634 if _scm.is_empty():
632 635 return []
633 636 except RepositoryError:
634 637 log.exception("Exception occurred while trying to get repo nodes")
635 638 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
636 639
637 640 try:
638 641 # we need to resolve commit_id to a FULL sha for cache to work correctly.
639 642 # sending 'master' is a pointer that needs to be translated to current commit.
640 643 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
641 644 log.debug(
642 645 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
643 646 'with caching: %s[TTL: %ss]' % (
644 647 repo_id, commit_id, cache_on, cache_seconds or 0))
645 648
646 649 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
647 650 return tree_files
648 651
649 652 except Exception:
650 653 log.exception("Exception occurred while trying to get repo nodes")
651 654 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
652 655
653 656
654 657 @jsonrpc_method()
655 658 def get_repo_refs(request, apiuser, repoid):
656 659 """
657 660 Returns a dictionary of current references. It returns
658 661 bookmarks, branches, closed_branches, and tags for given repository
659 662
660 663 It's possible to specify ret_type to show only `files` or `dirs`.
661 664
662 665 This command can only be run using an |authtoken| with admin rights,
663 666 or users with at least read rights to |repos|.
664 667
665 668 :param apiuser: This is filled automatically from the |authtoken|.
666 669 :type apiuser: AuthUser
667 670 :param repoid: The repository name or repository ID.
668 671 :type repoid: str or int
669 672
670 673 Example output:
671 674
672 675 .. code-block:: bash
673 676
674 677 id : <id_given_in_input>
675 678 "result": {
676 679 "bookmarks": {
677 680 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
678 681 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
679 682 },
680 683 "branches": {
681 684 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
682 685 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
683 686 },
684 687 "branches_closed": {},
685 688 "tags": {
686 689 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
687 690 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
688 691 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
689 692 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
690 693 }
691 694 }
692 695 error: null
693 696 """
694 697
695 698 repo = get_repo_or_error(repoid)
696 699 if not has_superadmin_permission(apiuser):
697 700 _perms = ('repository.admin', 'repository.write', 'repository.read',)
698 701 validate_repo_permissions(apiuser, repoid, repo, _perms)
699 702
700 703 try:
701 704 # check if repo is not empty by any chance, skip quicker if it is.
702 705 vcs_instance = repo.scm_instance()
703 706 refs = vcs_instance.refs()
704 707 return refs
705 708 except Exception:
706 709 log.exception("Exception occurred while trying to get repo refs")
707 710 raise JSONRPCError(
708 711 'failed to get repo: `%s` references' % repo.repo_name
709 712 )
710 713
711 714
712 715 @jsonrpc_method()
713 716 def create_repo(
714 717 request, apiuser, repo_name, repo_type,
715 718 owner=Optional(OAttr('apiuser')),
716 719 description=Optional(''),
717 720 private=Optional(False),
718 721 clone_uri=Optional(None),
719 722 push_uri=Optional(None),
720 723 landing_rev=Optional(None),
721 724 enable_statistics=Optional(False),
722 725 enable_locking=Optional(False),
723 726 enable_downloads=Optional(False),
724 727 copy_permissions=Optional(False)):
725 728 """
726 729 Creates a repository.
727 730
728 731 * If the repository name contains "/", repository will be created inside
729 732 a repository group or nested repository groups
730 733
731 734 For example "foo/bar/repo1" will create |repo| called "repo1" inside
732 735 group "foo/bar". You have to have permissions to access and write to
733 736 the last repository group ("bar" in this example)
734 737
735 738 This command can only be run using an |authtoken| with at least
736 739 permissions to create repositories, or write permissions to
737 740 parent repository groups.
738 741
739 742 :param apiuser: This is filled automatically from the |authtoken|.
740 743 :type apiuser: AuthUser
741 744 :param repo_name: Set the repository name.
742 745 :type repo_name: str
743 746 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
744 747 :type repo_type: str
745 748 :param owner: user_id or username
746 749 :type owner: Optional(str)
747 750 :param description: Set the repository description.
748 751 :type description: Optional(str)
749 752 :param private: set repository as private
750 753 :type private: bool
751 754 :param clone_uri: set clone_uri
752 755 :type clone_uri: str
753 756 :param push_uri: set push_uri
754 757 :type push_uri: str
755 758 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
756 759 :type landing_rev: str
757 760 :param enable_locking:
758 761 :type enable_locking: bool
759 762 :param enable_downloads:
760 763 :type enable_downloads: bool
761 764 :param enable_statistics:
762 765 :type enable_statistics: bool
763 766 :param copy_permissions: Copy permission from group in which the
764 767 repository is being created.
765 768 :type copy_permissions: bool
766 769
767 770
768 771 Example output:
769 772
770 773 .. code-block:: bash
771 774
772 775 id : <id_given_in_input>
773 776 result: {
774 777 "msg": "Created new repository `<reponame>`",
775 778 "success": true,
776 779 "task": "<celery task id or None if done sync>"
777 780 }
778 781 error: null
779 782
780 783
781 784 Example error output:
782 785
783 786 .. code-block:: bash
784 787
785 788 id : <id_given_in_input>
786 789 result : null
787 790 error : {
788 791 'failed to create repository `<repo_name>`'
789 792 }
790 793
791 794 """
792 795
793 796 owner = validate_set_owner_permissions(apiuser, owner)
794 797
795 798 description = Optional.extract(description)
796 799 copy_permissions = Optional.extract(copy_permissions)
797 800 clone_uri = Optional.extract(clone_uri)
798 801 push_uri = Optional.extract(push_uri)
799 802
800 803 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
801 804 if isinstance(private, Optional):
802 805 private = defs.get('repo_private') or Optional.extract(private)
803 806 if isinstance(repo_type, Optional):
804 807 repo_type = defs.get('repo_type')
805 808 if isinstance(enable_statistics, Optional):
806 809 enable_statistics = defs.get('repo_enable_statistics')
807 810 if isinstance(enable_locking, Optional):
808 811 enable_locking = defs.get('repo_enable_locking')
809 812 if isinstance(enable_downloads, Optional):
810 813 enable_downloads = defs.get('repo_enable_downloads')
811 814
812 815 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
813 816 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
814 817 ref_choices = list(set(ref_choices + [landing_ref]))
815 818
816 819 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
817 820
818 821 schema = repo_schema.RepoSchema().bind(
819 822 repo_type_options=rhodecode.BACKENDS.keys(),
820 823 repo_ref_options=ref_choices,
821 824 repo_type=repo_type,
822 825 # user caller
823 826 user=apiuser)
824 827
825 828 try:
826 829 schema_data = schema.deserialize(dict(
827 830 repo_name=repo_name,
828 831 repo_type=repo_type,
829 832 repo_owner=owner.username,
830 833 repo_description=description,
831 834 repo_landing_commit_ref=landing_commit_ref,
832 835 repo_clone_uri=clone_uri,
833 836 repo_push_uri=push_uri,
834 837 repo_private=private,
835 838 repo_copy_permissions=copy_permissions,
836 839 repo_enable_statistics=enable_statistics,
837 840 repo_enable_downloads=enable_downloads,
838 841 repo_enable_locking=enable_locking))
839 842 except validation_schema.Invalid as err:
840 843 raise JSONRPCValidationError(colander_exc=err)
841 844
842 845 try:
843 846 data = {
844 847 'owner': owner,
845 848 'repo_name': schema_data['repo_group']['repo_name_without_group'],
846 849 'repo_name_full': schema_data['repo_name'],
847 850 'repo_group': schema_data['repo_group']['repo_group_id'],
848 851 'repo_type': schema_data['repo_type'],
849 852 'repo_description': schema_data['repo_description'],
850 853 'repo_private': schema_data['repo_private'],
851 854 'clone_uri': schema_data['repo_clone_uri'],
852 855 'push_uri': schema_data['repo_push_uri'],
853 856 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
854 857 'enable_statistics': schema_data['repo_enable_statistics'],
855 858 'enable_locking': schema_data['repo_enable_locking'],
856 859 'enable_downloads': schema_data['repo_enable_downloads'],
857 860 'repo_copy_permissions': schema_data['repo_copy_permissions'],
858 861 }
859 862
860 863 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
861 864 task_id = get_task_id(task)
862 865 # no commit, it's done in RepoModel, or async via celery
863 866 return {
864 867 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
865 868 'success': True, # cannot return the repo data here since fork
866 869 # can be done async
867 870 'task': task_id
868 871 }
869 872 except Exception:
870 873 log.exception(
871 874 u"Exception while trying to create the repository %s",
872 875 schema_data['repo_name'])
873 876 raise JSONRPCError(
874 877 'failed to create repository `%s`' % (schema_data['repo_name'],))
875 878
876 879
877 880 @jsonrpc_method()
878 881 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
879 882 description=Optional('')):
880 883 """
881 884 Adds an extra field to a repository.
882 885
883 886 This command can only be run using an |authtoken| with at least
884 887 write permissions to the |repo|.
885 888
886 889 :param apiuser: This is filled automatically from the |authtoken|.
887 890 :type apiuser: AuthUser
888 891 :param repoid: Set the repository name or repository id.
889 892 :type repoid: str or int
890 893 :param key: Create a unique field key for this repository.
891 894 :type key: str
892 895 :param label:
893 896 :type label: Optional(str)
894 897 :param description:
895 898 :type description: Optional(str)
896 899 """
897 900 repo = get_repo_or_error(repoid)
898 901 if not has_superadmin_permission(apiuser):
899 902 _perms = ('repository.admin',)
900 903 validate_repo_permissions(apiuser, repoid, repo, _perms)
901 904
902 905 label = Optional.extract(label) or key
903 906 description = Optional.extract(description)
904 907
905 908 field = RepositoryField.get_by_key_name(key, repo)
906 909 if field:
907 910 raise JSONRPCError('Field with key '
908 911 '`%s` exists for repo `%s`' % (key, repoid))
909 912
910 913 try:
911 914 RepoModel().add_repo_field(repo, key, field_label=label,
912 915 field_desc=description)
913 916 Session().commit()
914 917 return {
915 918 'msg': "Added new repository field `%s`" % (key,),
916 919 'success': True,
917 920 }
918 921 except Exception:
919 922 log.exception("Exception occurred while trying to add field to repo")
920 923 raise JSONRPCError(
921 924 'failed to create new field for repository `%s`' % (repoid,))
922 925
923 926
924 927 @jsonrpc_method()
925 928 def remove_field_from_repo(request, apiuser, repoid, key):
926 929 """
927 930 Removes an extra field from a repository.
928 931
929 932 This command can only be run using an |authtoken| with at least
930 933 write permissions to the |repo|.
931 934
932 935 :param apiuser: This is filled automatically from the |authtoken|.
933 936 :type apiuser: AuthUser
934 937 :param repoid: Set the repository name or repository ID.
935 938 :type repoid: str or int
936 939 :param key: Set the unique field key for this repository.
937 940 :type key: str
938 941 """
939 942
940 943 repo = get_repo_or_error(repoid)
941 944 if not has_superadmin_permission(apiuser):
942 945 _perms = ('repository.admin',)
943 946 validate_repo_permissions(apiuser, repoid, repo, _perms)
944 947
945 948 field = RepositoryField.get_by_key_name(key, repo)
946 949 if not field:
947 950 raise JSONRPCError('Field with key `%s` does not '
948 951 'exists for repo `%s`' % (key, repoid))
949 952
950 953 try:
951 954 RepoModel().delete_repo_field(repo, field_key=key)
952 955 Session().commit()
953 956 return {
954 957 'msg': "Deleted repository field `%s`" % (key,),
955 958 'success': True,
956 959 }
957 960 except Exception:
958 961 log.exception(
959 962 "Exception occurred while trying to delete field from repo")
960 963 raise JSONRPCError(
961 964 'failed to delete field for repository `%s`' % (repoid,))
962 965
963 966
964 967 @jsonrpc_method()
965 968 def update_repo(
966 969 request, apiuser, repoid, repo_name=Optional(None),
967 970 owner=Optional(OAttr('apiuser')), description=Optional(''),
968 971 private=Optional(False),
969 972 clone_uri=Optional(None), push_uri=Optional(None),
970 973 landing_rev=Optional(None), fork_of=Optional(None),
971 974 enable_statistics=Optional(False),
972 975 enable_locking=Optional(False),
973 976 enable_downloads=Optional(False), fields=Optional('')):
974 977 """
975 978 Updates a repository with the given information.
976 979
977 980 This command can only be run using an |authtoken| with at least
978 981 admin permissions to the |repo|.
979 982
980 983 * If the repository name contains "/", repository will be updated
981 984 accordingly with a repository group or nested repository groups
982 985
983 986 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
984 987 called "repo-test" and place it inside group "foo/bar".
985 988 You have to have permissions to access and write to the last repository
986 989 group ("bar" in this example)
987 990
988 991 :param apiuser: This is filled automatically from the |authtoken|.
989 992 :type apiuser: AuthUser
990 993 :param repoid: repository name or repository ID.
991 994 :type repoid: str or int
992 995 :param repo_name: Update the |repo| name, including the
993 996 repository group it's in.
994 997 :type repo_name: str
995 998 :param owner: Set the |repo| owner.
996 999 :type owner: str
997 1000 :param fork_of: Set the |repo| as fork of another |repo|.
998 1001 :type fork_of: str
999 1002 :param description: Update the |repo| description.
1000 1003 :type description: str
1001 1004 :param private: Set the |repo| as private. (True | False)
1002 1005 :type private: bool
1003 1006 :param clone_uri: Update the |repo| clone URI.
1004 1007 :type clone_uri: str
1005 1008 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1006 1009 :type landing_rev: str
1007 1010 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1008 1011 :type enable_statistics: bool
1009 1012 :param enable_locking: Enable |repo| locking.
1010 1013 :type enable_locking: bool
1011 1014 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1012 1015 :type enable_downloads: bool
1013 1016 :param fields: Add extra fields to the |repo|. Use the following
1014 1017 example format: ``field_key=field_val,field_key2=fieldval2``.
1015 1018 Escape ', ' with \,
1016 1019 :type fields: str
1017 1020 """
1018 1021
1019 1022 repo = get_repo_or_error(repoid)
1020 1023
1021 1024 include_secrets = False
1022 1025 if not has_superadmin_permission(apiuser):
1023 1026 _perms = ('repository.admin',)
1024 1027 validate_repo_permissions(apiuser, repoid, repo, _perms)
1025 1028 else:
1026 1029 include_secrets = True
1027 1030
1028 1031 updates = dict(
1029 1032 repo_name=repo_name
1030 1033 if not isinstance(repo_name, Optional) else repo.repo_name,
1031 1034
1032 1035 fork_id=fork_of
1033 1036 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1034 1037
1035 1038 user=owner
1036 1039 if not isinstance(owner, Optional) else repo.user.username,
1037 1040
1038 1041 repo_description=description
1039 1042 if not isinstance(description, Optional) else repo.description,
1040 1043
1041 1044 repo_private=private
1042 1045 if not isinstance(private, Optional) else repo.private,
1043 1046
1044 1047 clone_uri=clone_uri
1045 1048 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1046 1049
1047 1050 push_uri=push_uri
1048 1051 if not isinstance(push_uri, Optional) else repo.push_uri,
1049 1052
1050 1053 repo_landing_rev=landing_rev
1051 1054 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1052 1055
1053 1056 repo_enable_statistics=enable_statistics
1054 1057 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1055 1058
1056 1059 repo_enable_locking=enable_locking
1057 1060 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1058 1061
1059 1062 repo_enable_downloads=enable_downloads
1060 1063 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1061 1064
1062 1065 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1063 1066 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1064 1067 request.translate, repo=repo)
1065 1068 ref_choices = list(set(ref_choices + [landing_ref]))
1066 1069
1067 1070 old_values = repo.get_api_data()
1068 1071 repo_type = repo.repo_type
1069 1072 schema = repo_schema.RepoSchema().bind(
1070 1073 repo_type_options=rhodecode.BACKENDS.keys(),
1071 1074 repo_ref_options=ref_choices,
1072 1075 repo_type=repo_type,
1073 1076 # user caller
1074 1077 user=apiuser,
1075 1078 old_values=old_values)
1076 1079 try:
1077 1080 schema_data = schema.deserialize(dict(
1078 1081 # we save old value, users cannot change type
1079 1082 repo_type=repo_type,
1080 1083
1081 1084 repo_name=updates['repo_name'],
1082 1085 repo_owner=updates['user'],
1083 1086 repo_description=updates['repo_description'],
1084 1087 repo_clone_uri=updates['clone_uri'],
1085 1088 repo_push_uri=updates['push_uri'],
1086 1089 repo_fork_of=updates['fork_id'],
1087 1090 repo_private=updates['repo_private'],
1088 1091 repo_landing_commit_ref=updates['repo_landing_rev'],
1089 1092 repo_enable_statistics=updates['repo_enable_statistics'],
1090 1093 repo_enable_downloads=updates['repo_enable_downloads'],
1091 1094 repo_enable_locking=updates['repo_enable_locking']))
1092 1095 except validation_schema.Invalid as err:
1093 1096 raise JSONRPCValidationError(colander_exc=err)
1094 1097
1095 1098 # save validated data back into the updates dict
1096 1099 validated_updates = dict(
1097 1100 repo_name=schema_data['repo_group']['repo_name_without_group'],
1098 1101 repo_group=schema_data['repo_group']['repo_group_id'],
1099 1102
1100 1103 user=schema_data['repo_owner'],
1101 1104 repo_description=schema_data['repo_description'],
1102 1105 repo_private=schema_data['repo_private'],
1103 1106 clone_uri=schema_data['repo_clone_uri'],
1104 1107 push_uri=schema_data['repo_push_uri'],
1105 1108 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1106 1109 repo_enable_statistics=schema_data['repo_enable_statistics'],
1107 1110 repo_enable_locking=schema_data['repo_enable_locking'],
1108 1111 repo_enable_downloads=schema_data['repo_enable_downloads'],
1109 1112 )
1110 1113
1111 1114 if schema_data['repo_fork_of']:
1112 1115 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1113 1116 validated_updates['fork_id'] = fork_repo.repo_id
1114 1117
1115 1118 # extra fields
1116 1119 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1117 1120 if fields:
1118 1121 validated_updates.update(fields)
1119 1122
1120 1123 try:
1121 1124 RepoModel().update(repo, **validated_updates)
1122 1125 audit_logger.store_api(
1123 1126 'repo.edit', action_data={'old_data': old_values},
1124 1127 user=apiuser, repo=repo)
1125 1128 Session().commit()
1126 1129 return {
1127 1130 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1128 1131 'repository': repo.get_api_data(include_secrets=include_secrets)
1129 1132 }
1130 1133 except Exception:
1131 1134 log.exception(
1132 1135 u"Exception while trying to update the repository %s",
1133 1136 repoid)
1134 1137 raise JSONRPCError('failed to update repo `%s`' % repoid)
1135 1138
1136 1139
1137 1140 @jsonrpc_method()
1138 1141 def fork_repo(request, apiuser, repoid, fork_name,
1139 1142 owner=Optional(OAttr('apiuser')),
1140 1143 description=Optional(''),
1141 1144 private=Optional(False),
1142 1145 clone_uri=Optional(None),
1143 1146 landing_rev=Optional(None),
1144 1147 copy_permissions=Optional(False)):
1145 1148 """
1146 1149 Creates a fork of the specified |repo|.
1147 1150
1148 1151 * If the fork_name contains "/", fork will be created inside
1149 1152 a repository group or nested repository groups
1150 1153
1151 1154 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1152 1155 inside group "foo/bar". You have to have permissions to access and
1153 1156 write to the last repository group ("bar" in this example)
1154 1157
1155 1158 This command can only be run using an |authtoken| with minimum
1156 1159 read permissions of the forked repo, create fork permissions for an user.
1157 1160
1158 1161 :param apiuser: This is filled automatically from the |authtoken|.
1159 1162 :type apiuser: AuthUser
1160 1163 :param repoid: Set repository name or repository ID.
1161 1164 :type repoid: str or int
1162 1165 :param fork_name: Set the fork name, including it's repository group membership.
1163 1166 :type fork_name: str
1164 1167 :param owner: Set the fork owner.
1165 1168 :type owner: str
1166 1169 :param description: Set the fork description.
1167 1170 :type description: str
1168 1171 :param copy_permissions: Copy permissions from parent |repo|. The
1169 1172 default is False.
1170 1173 :type copy_permissions: bool
1171 1174 :param private: Make the fork private. The default is False.
1172 1175 :type private: bool
1173 1176 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1174 1177
1175 1178 Example output:
1176 1179
1177 1180 .. code-block:: bash
1178 1181
1179 1182 id : <id_for_response>
1180 1183 api_key : "<api_key>"
1181 1184 args: {
1182 1185 "repoid" : "<reponame or repo_id>",
1183 1186 "fork_name": "<forkname>",
1184 1187 "owner": "<username or user_id = Optional(=apiuser)>",
1185 1188 "description": "<description>",
1186 1189 "copy_permissions": "<bool>",
1187 1190 "private": "<bool>",
1188 1191 "landing_rev": "<landing_rev>"
1189 1192 }
1190 1193
1191 1194 Example error output:
1192 1195
1193 1196 .. code-block:: bash
1194 1197
1195 1198 id : <id_given_in_input>
1196 1199 result: {
1197 1200 "msg": "Created fork of `<reponame>` as `<forkname>`",
1198 1201 "success": true,
1199 1202 "task": "<celery task id or None if done sync>"
1200 1203 }
1201 1204 error: null
1202 1205
1203 1206 """
1204 1207
1205 1208 repo = get_repo_or_error(repoid)
1206 1209 repo_name = repo.repo_name
1207 1210
1208 1211 if not has_superadmin_permission(apiuser):
1209 1212 # check if we have at least read permission for
1210 1213 # this repo that we fork !
1211 1214 _perms = ('repository.admin', 'repository.write', 'repository.read')
1212 1215 validate_repo_permissions(apiuser, repoid, repo, _perms)
1213 1216
1214 1217 # check if the regular user has at least fork permissions as well
1215 1218 if not HasPermissionAnyApi(PermissionModel.FORKING_ENABLED)(user=apiuser):
1216 1219 raise JSONRPCForbidden()
1217 1220
1218 1221 # check if user can set owner parameter
1219 1222 owner = validate_set_owner_permissions(apiuser, owner)
1220 1223
1221 1224 description = Optional.extract(description)
1222 1225 copy_permissions = Optional.extract(copy_permissions)
1223 1226 clone_uri = Optional.extract(clone_uri)
1224 1227
1225 1228 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1226 1229 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1227 1230 ref_choices = list(set(ref_choices + [landing_ref]))
1228 1231 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1229 1232
1230 1233 private = Optional.extract(private)
1231 1234
1232 1235 schema = repo_schema.RepoSchema().bind(
1233 1236 repo_type_options=rhodecode.BACKENDS.keys(),
1234 1237 repo_ref_options=ref_choices,
1235 1238 repo_type=repo.repo_type,
1236 1239 # user caller
1237 1240 user=apiuser)
1238 1241
1239 1242 try:
1240 1243 schema_data = schema.deserialize(dict(
1241 1244 repo_name=fork_name,
1242 1245 repo_type=repo.repo_type,
1243 1246 repo_owner=owner.username,
1244 1247 repo_description=description,
1245 1248 repo_landing_commit_ref=landing_commit_ref,
1246 1249 repo_clone_uri=clone_uri,
1247 1250 repo_private=private,
1248 1251 repo_copy_permissions=copy_permissions))
1249 1252 except validation_schema.Invalid as err:
1250 1253 raise JSONRPCValidationError(colander_exc=err)
1251 1254
1252 1255 try:
1253 1256 data = {
1254 1257 'fork_parent_id': repo.repo_id,
1255 1258
1256 1259 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1257 1260 'repo_name_full': schema_data['repo_name'],
1258 1261 'repo_group': schema_data['repo_group']['repo_group_id'],
1259 1262 'repo_type': schema_data['repo_type'],
1260 1263 'description': schema_data['repo_description'],
1261 1264 'private': schema_data['repo_private'],
1262 1265 'copy_permissions': schema_data['repo_copy_permissions'],
1263 1266 'landing_rev': schema_data['repo_landing_commit_ref'],
1264 1267 }
1265 1268
1266 1269 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1267 1270 # no commit, it's done in RepoModel, or async via celery
1268 1271 task_id = get_task_id(task)
1269 1272
1270 1273 return {
1271 1274 'msg': 'Created fork of `%s` as `%s`' % (
1272 1275 repo.repo_name, schema_data['repo_name']),
1273 1276 'success': True, # cannot return the repo data here since fork
1274 1277 # can be done async
1275 1278 'task': task_id
1276 1279 }
1277 1280 except Exception:
1278 1281 log.exception(
1279 1282 u"Exception while trying to create fork %s",
1280 1283 schema_data['repo_name'])
1281 1284 raise JSONRPCError(
1282 1285 'failed to fork repository `%s` as `%s`' % (
1283 1286 repo_name, schema_data['repo_name']))
1284 1287
1285 1288
1286 1289 @jsonrpc_method()
1287 1290 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1288 1291 """
1289 1292 Deletes a repository.
1290 1293
1291 1294 * When the `forks` parameter is set it's possible to detach or delete
1292 1295 forks of deleted repository.
1293 1296
1294 1297 This command can only be run using an |authtoken| with admin
1295 1298 permissions on the |repo|.
1296 1299
1297 1300 :param apiuser: This is filled automatically from the |authtoken|.
1298 1301 :type apiuser: AuthUser
1299 1302 :param repoid: Set the repository name or repository ID.
1300 1303 :type repoid: str or int
1301 1304 :param forks: Set to `detach` or `delete` forks from the |repo|.
1302 1305 :type forks: Optional(str)
1303 1306
1304 1307 Example error output:
1305 1308
1306 1309 .. code-block:: bash
1307 1310
1308 1311 id : <id_given_in_input>
1309 1312 result: {
1310 1313 "msg": "Deleted repository `<reponame>`",
1311 1314 "success": true
1312 1315 }
1313 1316 error: null
1314 1317 """
1315 1318
1316 1319 repo = get_repo_or_error(repoid)
1317 1320 repo_name = repo.repo_name
1318 1321 if not has_superadmin_permission(apiuser):
1319 1322 _perms = ('repository.admin',)
1320 1323 validate_repo_permissions(apiuser, repoid, repo, _perms)
1321 1324
1322 1325 try:
1323 1326 handle_forks = Optional.extract(forks)
1324 1327 _forks_msg = ''
1325 1328 _forks = [f for f in repo.forks]
1326 1329 if handle_forks == 'detach':
1327 1330 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1328 1331 elif handle_forks == 'delete':
1329 1332 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1330 1333 elif _forks:
1331 1334 raise JSONRPCError(
1332 1335 'Cannot delete `%s` it still contains attached forks' %
1333 1336 (repo.repo_name,)
1334 1337 )
1335 1338 old_data = repo.get_api_data()
1336 1339 RepoModel().delete(repo, forks=forks)
1337 1340
1338 1341 repo = audit_logger.RepoWrap(repo_id=None,
1339 1342 repo_name=repo.repo_name)
1340 1343
1341 1344 audit_logger.store_api(
1342 1345 'repo.delete', action_data={'old_data': old_data},
1343 1346 user=apiuser, repo=repo)
1344 1347
1345 1348 ScmModel().mark_for_invalidation(repo_name, delete=True)
1346 1349 Session().commit()
1347 1350 return {
1348 1351 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1349 1352 'success': True
1350 1353 }
1351 1354 except Exception:
1352 1355 log.exception("Exception occurred while trying to delete repo")
1353 1356 raise JSONRPCError(
1354 1357 'failed to delete repository `%s`' % (repo_name,)
1355 1358 )
1356 1359
1357 1360
1358 1361 #TODO: marcink, change name ?
1359 1362 @jsonrpc_method()
1360 1363 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1361 1364 """
1362 1365 Invalidates the cache for the specified repository.
1363 1366
1364 1367 This command can only be run using an |authtoken| with admin rights to
1365 1368 the specified repository.
1366 1369
1367 1370 This command takes the following options:
1368 1371
1369 1372 :param apiuser: This is filled automatically from |authtoken|.
1370 1373 :type apiuser: AuthUser
1371 1374 :param repoid: Sets the repository name or repository ID.
1372 1375 :type repoid: str or int
1373 1376 :param delete_keys: This deletes the invalidated keys instead of
1374 1377 just flagging them.
1375 1378 :type delete_keys: Optional(``True`` | ``False``)
1376 1379
1377 1380 Example output:
1378 1381
1379 1382 .. code-block:: bash
1380 1383
1381 1384 id : <id_given_in_input>
1382 1385 result : {
1383 1386 'msg': Cache for repository `<repository name>` was invalidated,
1384 1387 'repository': <repository name>
1385 1388 }
1386 1389 error : null
1387 1390
1388 1391 Example error output:
1389 1392
1390 1393 .. code-block:: bash
1391 1394
1392 1395 id : <id_given_in_input>
1393 1396 result : null
1394 1397 error : {
1395 1398 'Error occurred during cache invalidation action'
1396 1399 }
1397 1400
1398 1401 """
1399 1402
1400 1403 repo = get_repo_or_error(repoid)
1401 1404 if not has_superadmin_permission(apiuser):
1402 1405 _perms = ('repository.admin', 'repository.write',)
1403 1406 validate_repo_permissions(apiuser, repoid, repo, _perms)
1404 1407
1405 1408 delete = Optional.extract(delete_keys)
1406 1409 try:
1407 1410 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1408 1411 return {
1409 1412 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1410 1413 'repository': repo.repo_name
1411 1414 }
1412 1415 except Exception:
1413 1416 log.exception(
1414 1417 "Exception occurred while trying to invalidate repo cache")
1415 1418 raise JSONRPCError(
1416 1419 'Error occurred during cache invalidation action'
1417 1420 )
1418 1421
1419 1422
1420 1423 #TODO: marcink, change name ?
1421 1424 @jsonrpc_method()
1422 1425 def lock(request, apiuser, repoid, locked=Optional(None),
1423 1426 userid=Optional(OAttr('apiuser'))):
1424 1427 """
1425 1428 Sets the lock state of the specified |repo| by the given user.
1426 1429 From more information, see :ref:`repo-locking`.
1427 1430
1428 1431 * If the ``userid`` option is not set, the repository is locked to the
1429 1432 user who called the method.
1430 1433 * If the ``locked`` parameter is not set, the current lock state of the
1431 1434 repository is displayed.
1432 1435
1433 1436 This command can only be run using an |authtoken| with admin rights to
1434 1437 the specified repository.
1435 1438
1436 1439 This command takes the following options:
1437 1440
1438 1441 :param apiuser: This is filled automatically from the |authtoken|.
1439 1442 :type apiuser: AuthUser
1440 1443 :param repoid: Sets the repository name or repository ID.
1441 1444 :type repoid: str or int
1442 1445 :param locked: Sets the lock state.
1443 1446 :type locked: Optional(``True`` | ``False``)
1444 1447 :param userid: Set the repository lock to this user.
1445 1448 :type userid: Optional(str or int)
1446 1449
1447 1450 Example error output:
1448 1451
1449 1452 .. code-block:: bash
1450 1453
1451 1454 id : <id_given_in_input>
1452 1455 result : {
1453 1456 'repo': '<reponame>',
1454 1457 'locked': <bool: lock state>,
1455 1458 'locked_since': <int: lock timestamp>,
1456 1459 'locked_by': <username of person who made the lock>,
1457 1460 'lock_reason': <str: reason for locking>,
1458 1461 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1459 1462 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1460 1463 or
1461 1464 'msg': 'Repo `<repository name>` not locked.'
1462 1465 or
1463 1466 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1464 1467 }
1465 1468 error : null
1466 1469
1467 1470 Example error output:
1468 1471
1469 1472 .. code-block:: bash
1470 1473
1471 1474 id : <id_given_in_input>
1472 1475 result : null
1473 1476 error : {
1474 1477 'Error occurred locking repository `<reponame>`'
1475 1478 }
1476 1479 """
1477 1480
1478 1481 repo = get_repo_or_error(repoid)
1479 1482 if not has_superadmin_permission(apiuser):
1480 1483 # check if we have at least write permission for this repo !
1481 1484 _perms = ('repository.admin', 'repository.write',)
1482 1485 validate_repo_permissions(apiuser, repoid, repo, _perms)
1483 1486
1484 1487 # make sure normal user does not pass someone else userid,
1485 1488 # he is not allowed to do that
1486 1489 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1487 1490 raise JSONRPCError('userid is not the same as your user')
1488 1491
1489 1492 if isinstance(userid, Optional):
1490 1493 userid = apiuser.user_id
1491 1494
1492 1495 user = get_user_or_error(userid)
1493 1496
1494 1497 if isinstance(locked, Optional):
1495 1498 lockobj = repo.locked
1496 1499
1497 1500 if lockobj[0] is None:
1498 1501 _d = {
1499 1502 'repo': repo.repo_name,
1500 1503 'locked': False,
1501 1504 'locked_since': None,
1502 1505 'locked_by': None,
1503 1506 'lock_reason': None,
1504 1507 'lock_state_changed': False,
1505 1508 'msg': 'Repo `%s` not locked.' % repo.repo_name
1506 1509 }
1507 1510 return _d
1508 1511 else:
1509 1512 _user_id, _time, _reason = lockobj
1510 1513 lock_user = get_user_or_error(userid)
1511 1514 _d = {
1512 1515 'repo': repo.repo_name,
1513 1516 'locked': True,
1514 1517 'locked_since': _time,
1515 1518 'locked_by': lock_user.username,
1516 1519 'lock_reason': _reason,
1517 1520 'lock_state_changed': False,
1518 1521 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1519 1522 % (repo.repo_name, lock_user.username,
1520 1523 json.dumps(time_to_datetime(_time))))
1521 1524 }
1522 1525 return _d
1523 1526
1524 1527 # force locked state through a flag
1525 1528 else:
1526 1529 locked = str2bool(locked)
1527 1530 lock_reason = Repository.LOCK_API
1528 1531 try:
1529 1532 if locked:
1530 1533 lock_time = time.time()
1531 1534 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1532 1535 else:
1533 1536 lock_time = None
1534 1537 Repository.unlock(repo)
1535 1538 _d = {
1536 1539 'repo': repo.repo_name,
1537 1540 'locked': locked,
1538 1541 'locked_since': lock_time,
1539 1542 'locked_by': user.username,
1540 1543 'lock_reason': lock_reason,
1541 1544 'lock_state_changed': True,
1542 1545 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1543 1546 % (user.username, repo.repo_name, locked))
1544 1547 }
1545 1548 return _d
1546 1549 except Exception:
1547 1550 log.exception(
1548 1551 "Exception occurred while trying to lock repository")
1549 1552 raise JSONRPCError(
1550 1553 'Error occurred locking repository `%s`' % repo.repo_name
1551 1554 )
1552 1555
1553 1556
1554 1557 @jsonrpc_method()
1555 1558 def comment_commit(
1556 1559 request, apiuser, repoid, commit_id, message, status=Optional(None),
1557 1560 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1558 1561 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1559 1562 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1560 1563 """
1561 1564 Set a commit comment, and optionally change the status of the commit.
1562 1565
1563 1566 :param apiuser: This is filled automatically from the |authtoken|.
1564 1567 :type apiuser: AuthUser
1565 1568 :param repoid: Set the repository name or repository ID.
1566 1569 :type repoid: str or int
1567 1570 :param commit_id: Specify the commit_id for which to set a comment.
1568 1571 :type commit_id: str
1569 1572 :param message: The comment text.
1570 1573 :type message: str
1571 1574 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1572 1575 'approved', 'rejected', 'under_review'
1573 1576 :type status: str
1574 1577 :param comment_type: Comment type, one of: 'note', 'todo'
1575 1578 :type comment_type: Optional(str), default: 'note'
1576 1579 :param resolves_comment_id: id of comment which this one will resolve
1577 1580 :type resolves_comment_id: Optional(int)
1578 1581 :param extra_recipients: list of user ids or usernames to add
1579 1582 notifications for this comment. Acts like a CC for notification
1580 1583 :type extra_recipients: Optional(list)
1581 1584 :param userid: Set the user name of the comment creator.
1582 1585 :type userid: Optional(str or int)
1583 1586 :param send_email: Define if this comment should also send email notification
1584 1587 :type send_email: Optional(bool)
1585 1588
1586 1589 Example error output:
1587 1590
1588 1591 .. code-block:: bash
1589 1592
1590 1593 {
1591 1594 "id" : <id_given_in_input>,
1592 1595 "result" : {
1593 1596 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1594 1597 "status_change": null or <status>,
1595 1598 "success": true
1596 1599 },
1597 1600 "error" : null
1598 1601 }
1599 1602
1600 1603 """
1601 1604 _ = request.translate
1602 1605
1603 1606 repo = get_repo_or_error(repoid)
1604 1607 if not has_superadmin_permission(apiuser):
1605 1608 _perms = ('repository.read', 'repository.write', 'repository.admin')
1606 1609 validate_repo_permissions(apiuser, repoid, repo, _perms)
1607 1610 db_repo_name = repo.repo_name
1608 1611
1609 1612 try:
1610 1613 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1611 1614 commit_id = commit.raw_id
1612 1615 except Exception as e:
1613 1616 log.exception('Failed to fetch commit')
1614 1617 raise JSONRPCError(safe_str(e))
1615 1618
1616 1619 if isinstance(userid, Optional):
1617 1620 userid = apiuser.user_id
1618 1621
1619 1622 user = get_user_or_error(userid)
1620 1623 status = Optional.extract(status)
1621 1624 comment_type = Optional.extract(comment_type)
1622 1625 resolves_comment_id = Optional.extract(resolves_comment_id)
1623 1626 extra_recipients = Optional.extract(extra_recipients)
1624 1627 send_email = Optional.extract(send_email, binary=True)
1625 1628
1626 1629 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1627 1630 if status and status not in allowed_statuses:
1628 1631 raise JSONRPCError('Bad status, must be on '
1629 1632 'of %s got %s' % (allowed_statuses, status,))
1630 1633
1631 1634 if resolves_comment_id:
1632 1635 comment = ChangesetComment.get(resolves_comment_id)
1633 1636 if not comment:
1634 1637 raise JSONRPCError(
1635 1638 'Invalid resolves_comment_id `%s` for this commit.'
1636 1639 % resolves_comment_id)
1637 1640 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1638 1641 raise JSONRPCError(
1639 1642 'Comment `%s` is wrong type for setting status to resolved.'
1640 1643 % resolves_comment_id)
1641 1644
1642 1645 try:
1643 1646 rc_config = SettingsModel().get_all_settings()
1644 1647 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1645 1648 status_change_label = ChangesetStatus.get_status_lbl(status)
1646 1649 comment = CommentsModel().create(
1647 1650 message, repo, user, commit_id=commit_id,
1648 1651 status_change=status_change_label,
1649 1652 status_change_type=status,
1650 1653 renderer=renderer,
1651 1654 comment_type=comment_type,
1652 1655 resolves_comment_id=resolves_comment_id,
1653 1656 auth_user=apiuser,
1654 1657 extra_recipients=extra_recipients,
1655 1658 send_email=send_email
1656 1659 )
1657 1660 is_inline = comment.is_inline
1658 1661
1659 1662 if status:
1660 1663 # also do a status change
1661 1664 try:
1662 1665 ChangesetStatusModel().set_status(
1663 1666 repo, status, user, comment, revision=commit_id,
1664 1667 dont_allow_on_closed_pull_request=True
1665 1668 )
1666 1669 except StatusChangeOnClosedPullRequestError:
1667 1670 log.exception(
1668 1671 "Exception occurred while trying to change repo commit status")
1669 1672 msg = ('Changing status on a commit associated with '
1670 1673 'a closed pull request is not allowed')
1671 1674 raise JSONRPCError(msg)
1672 1675
1673 1676 CommentsModel().trigger_commit_comment_hook(
1674 1677 repo, apiuser, 'create',
1675 1678 data={'comment': comment, 'commit': commit})
1676 1679
1677 1680 Session().commit()
1678 1681
1679 1682 comment_broadcast_channel = channelstream.comment_channel(
1680 1683 db_repo_name, commit_obj=commit)
1681 1684
1682 1685 comment_data = {'comment': comment, 'comment_id': comment.comment_id}
1683 1686 comment_type = 'inline' if is_inline else 'general'
1684 1687 channelstream.comment_channelstream_push(
1685 1688 request, comment_broadcast_channel, apiuser,
1686 1689 _('posted a new {} comment').format(comment_type),
1687 1690 comment_data=comment_data)
1688 1691
1689 1692 return {
1690 1693 'msg': (
1691 1694 'Commented on commit `%s` for repository `%s`' % (
1692 1695 comment.revision, repo.repo_name)),
1693 1696 'status_change': status,
1694 1697 'success': True,
1695 1698 }
1696 1699 except JSONRPCError:
1697 1700 # catch any inside errors, and re-raise them to prevent from
1698 1701 # below global catch to silence them
1699 1702 raise
1700 1703 except Exception:
1701 1704 log.exception("Exception occurred while trying to comment on commit")
1702 1705 raise JSONRPCError(
1703 1706 'failed to set comment on repository `%s`' % (repo.repo_name,)
1704 1707 )
1705 1708
1706 1709
1707 1710 @jsonrpc_method()
1708 1711 def get_repo_comments(request, apiuser, repoid,
1709 1712 commit_id=Optional(None), comment_type=Optional(None),
1710 1713 userid=Optional(None)):
1711 1714 """
1712 1715 Get all comments for a repository
1713 1716
1714 1717 :param apiuser: This is filled automatically from the |authtoken|.
1715 1718 :type apiuser: AuthUser
1716 1719 :param repoid: Set the repository name or repository ID.
1717 1720 :type repoid: str or int
1718 1721 :param commit_id: Optionally filter the comments by the commit_id
1719 1722 :type commit_id: Optional(str), default: None
1720 1723 :param comment_type: Optionally filter the comments by the comment_type
1721 1724 one of: 'note', 'todo'
1722 1725 :type comment_type: Optional(str), default: None
1723 1726 :param userid: Optionally filter the comments by the author of comment
1724 1727 :type userid: Optional(str or int), Default: None
1725 1728
1726 1729 Example error output:
1727 1730
1728 1731 .. code-block:: bash
1729 1732
1730 1733 {
1731 1734 "id" : <id_given_in_input>,
1732 1735 "result" : [
1733 1736 {
1734 1737 "comment_author": <USER_DETAILS>,
1735 1738 "comment_created_on": "2017-02-01T14:38:16.309",
1736 1739 "comment_f_path": "file.txt",
1737 1740 "comment_id": 282,
1738 1741 "comment_lineno": "n1",
1739 1742 "comment_resolved_by": null,
1740 1743 "comment_status": [],
1741 1744 "comment_text": "This file needs a header",
1742 1745 "comment_type": "todo",
1743 1746 "comment_last_version: 0
1744 1747 }
1745 1748 ],
1746 1749 "error" : null
1747 1750 }
1748 1751
1749 1752 """
1750 1753 repo = get_repo_or_error(repoid)
1751 1754 if not has_superadmin_permission(apiuser):
1752 1755 _perms = ('repository.read', 'repository.write', 'repository.admin')
1753 1756 validate_repo_permissions(apiuser, repoid, repo, _perms)
1754 1757
1755 1758 commit_id = Optional.extract(commit_id)
1756 1759
1757 1760 userid = Optional.extract(userid)
1758 1761 if userid:
1759 1762 user = get_user_or_error(userid)
1760 1763 else:
1761 1764 user = None
1762 1765
1763 1766 comment_type = Optional.extract(comment_type)
1764 1767 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1765 1768 raise JSONRPCError(
1766 1769 'comment_type must be one of `{}` got {}'.format(
1767 1770 ChangesetComment.COMMENT_TYPES, comment_type)
1768 1771 )
1769 1772
1770 1773 comments = CommentsModel().get_repository_comments(
1771 1774 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1772 1775 return comments
1773 1776
1774 1777
1775 1778 @jsonrpc_method()
1776 1779 def get_comment(request, apiuser, comment_id):
1777 1780 """
1778 1781 Get single comment from repository or pull_request
1779 1782
1780 1783 :param apiuser: This is filled automatically from the |authtoken|.
1781 1784 :type apiuser: AuthUser
1782 1785 :param comment_id: comment id found in the URL of comment
1783 1786 :type comment_id: str or int
1784 1787
1785 1788 Example error output:
1786 1789
1787 1790 .. code-block:: bash
1788 1791
1789 1792 {
1790 1793 "id" : <id_given_in_input>,
1791 1794 "result" : {
1792 1795 "comment_author": <USER_DETAILS>,
1793 1796 "comment_created_on": "2017-02-01T14:38:16.309",
1794 1797 "comment_f_path": "file.txt",
1795 1798 "comment_id": 282,
1796 1799 "comment_lineno": "n1",
1797 1800 "comment_resolved_by": null,
1798 1801 "comment_status": [],
1799 1802 "comment_text": "This file needs a header",
1800 1803 "comment_type": "todo",
1801 1804 "comment_last_version: 0
1802 1805 },
1803 1806 "error" : null
1804 1807 }
1805 1808
1806 1809 """
1807 1810
1808 1811 comment = ChangesetComment.get(comment_id)
1809 1812 if not comment:
1810 1813 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1811 1814
1812 1815 perms = ('repository.read', 'repository.write', 'repository.admin')
1813 1816 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1814 1817 (user=apiuser, repo_name=comment.repo.repo_name)
1815 1818
1816 1819 if not has_comment_perm:
1817 1820 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1818 1821
1819 1822 return comment
1820 1823
1821 1824
1822 1825 @jsonrpc_method()
1823 1826 def edit_comment(request, apiuser, message, comment_id, version,
1824 1827 userid=Optional(OAttr('apiuser'))):
1825 1828 """
1826 1829 Edit comment on the pull request or commit,
1827 1830 specified by the `comment_id` and version. Initially version should be 0
1828 1831
1829 1832 :param apiuser: This is filled automatically from the |authtoken|.
1830 1833 :type apiuser: AuthUser
1831 1834 :param comment_id: Specify the comment_id for editing
1832 1835 :type comment_id: int
1833 1836 :param version: version of the comment that will be created, starts from 0
1834 1837 :type version: int
1835 1838 :param message: The text content of the comment.
1836 1839 :type message: str
1837 1840 :param userid: Comment on the pull request as this user
1838 1841 :type userid: Optional(str or int)
1839 1842
1840 1843 Example output:
1841 1844
1842 1845 .. code-block:: bash
1843 1846
1844 1847 id : <id_given_in_input>
1845 1848 result : {
1846 1849 "comment": "<comment data>",
1847 1850 "version": "<Integer>",
1848 1851 },
1849 1852 error : null
1850 1853 """
1851 1854
1852 1855 auth_user = apiuser
1853 1856 comment = ChangesetComment.get(comment_id)
1854 1857 if not comment:
1855 1858 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1856 1859
1857 1860 is_super_admin = has_superadmin_permission(apiuser)
1858 1861 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1859 1862 (user=apiuser, repo_name=comment.repo.repo_name)
1860 1863
1861 1864 if not isinstance(userid, Optional):
1862 1865 if is_super_admin or is_repo_admin:
1863 1866 apiuser = get_user_or_error(userid)
1864 1867 auth_user = apiuser.AuthUser()
1865 1868 else:
1866 1869 raise JSONRPCError('userid is not the same as your user')
1867 1870
1868 1871 comment_author = comment.author.user_id == auth_user.user_id
1869 1872 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1870 1873 raise JSONRPCError("you don't have access to edit this comment")
1871 1874
1872 1875 try:
1873 1876 comment_history = CommentsModel().edit(
1874 1877 comment_id=comment_id,
1875 1878 text=message,
1876 1879 auth_user=auth_user,
1877 1880 version=version,
1878 1881 )
1879 1882 Session().commit()
1880 1883 except CommentVersionMismatch:
1881 1884 raise JSONRPCError(
1882 1885 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1883 1886 )
1884 1887 if not comment_history and not message:
1885 1888 raise JSONRPCError(
1886 1889 "comment ({}) can't be changed with empty string".format(comment_id)
1887 1890 )
1888 1891
1889 1892 if comment.pull_request:
1890 1893 pull_request = comment.pull_request
1891 1894 PullRequestModel().trigger_pull_request_hook(
1892 1895 pull_request, apiuser, 'comment_edit',
1893 1896 data={'comment': comment})
1894 1897 else:
1895 1898 db_repo = comment.repo
1896 1899 commit_id = comment.revision
1897 1900 commit = db_repo.get_commit(commit_id)
1898 1901 CommentsModel().trigger_commit_comment_hook(
1899 1902 db_repo, apiuser, 'edit',
1900 1903 data={'comment': comment, 'commit': commit})
1901 1904
1902 1905 data = {
1903 1906 'comment': comment,
1904 1907 'version': comment_history.version if comment_history else None,
1905 1908 }
1906 1909 return data
1907 1910
1908 1911
1909 1912 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1910 1913 # @jsonrpc_method()
1911 1914 # def delete_comment(request, apiuser, comment_id):
1912 1915 # auth_user = apiuser
1913 1916 #
1914 1917 # comment = ChangesetComment.get(comment_id)
1915 1918 # if not comment:
1916 1919 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1917 1920 #
1918 1921 # is_super_admin = has_superadmin_permission(apiuser)
1919 1922 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1920 1923 # (user=apiuser, repo_name=comment.repo.repo_name)
1921 1924 #
1922 1925 # comment_author = comment.author.user_id == auth_user.user_id
1923 1926 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1924 1927 # raise JSONRPCError("you don't have access to edit this comment")
1925 1928
1926 1929 @jsonrpc_method()
1927 1930 def grant_user_permission(request, apiuser, repoid, userid, perm):
1928 1931 """
1929 1932 Grant permissions for the specified user on the given repository,
1930 1933 or update existing permissions if found.
1931 1934
1932 1935 This command can only be run using an |authtoken| with admin
1933 1936 permissions on the |repo|.
1934 1937
1935 1938 :param apiuser: This is filled automatically from the |authtoken|.
1936 1939 :type apiuser: AuthUser
1937 1940 :param repoid: Set the repository name or repository ID.
1938 1941 :type repoid: str or int
1939 1942 :param userid: Set the user name.
1940 1943 :type userid: str
1941 1944 :param perm: Set the user permissions, using the following format
1942 1945 ``(repository.(none|read|write|admin))``
1943 1946 :type perm: str
1944 1947
1945 1948 Example output:
1946 1949
1947 1950 .. code-block:: bash
1948 1951
1949 1952 id : <id_given_in_input>
1950 1953 result: {
1951 1954 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1952 1955 "success": true
1953 1956 }
1954 1957 error: null
1955 1958 """
1956 1959
1957 1960 repo = get_repo_or_error(repoid)
1958 1961 user = get_user_or_error(userid)
1959 1962 perm = get_perm_or_error(perm)
1960 1963 if not has_superadmin_permission(apiuser):
1961 1964 _perms = ('repository.admin',)
1962 1965 validate_repo_permissions(apiuser, repoid, repo, _perms)
1963 1966
1964 1967 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1965 1968 try:
1966 1969 changes = RepoModel().update_permissions(
1967 1970 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1968 1971
1969 1972 action_data = {
1970 1973 'added': changes['added'],
1971 1974 'updated': changes['updated'],
1972 1975 'deleted': changes['deleted'],
1973 1976 }
1974 1977 audit_logger.store_api(
1975 1978 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1976 1979 Session().commit()
1977 1980 PermissionModel().flush_user_permission_caches(changes)
1978 1981
1979 1982 return {
1980 1983 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1981 1984 perm.permission_name, user.username, repo.repo_name
1982 1985 ),
1983 1986 'success': True
1984 1987 }
1985 1988 except Exception:
1986 1989 log.exception("Exception occurred while trying edit permissions for repo")
1987 1990 raise JSONRPCError(
1988 1991 'failed to edit permission for user: `%s` in repo: `%s`' % (
1989 1992 userid, repoid
1990 1993 )
1991 1994 )
1992 1995
1993 1996
1994 1997 @jsonrpc_method()
1995 1998 def revoke_user_permission(request, apiuser, repoid, userid):
1996 1999 """
1997 2000 Revoke permission for a user on the specified repository.
1998 2001
1999 2002 This command can only be run using an |authtoken| with admin
2000 2003 permissions on the |repo|.
2001 2004
2002 2005 :param apiuser: This is filled automatically from the |authtoken|.
2003 2006 :type apiuser: AuthUser
2004 2007 :param repoid: Set the repository name or repository ID.
2005 2008 :type repoid: str or int
2006 2009 :param userid: Set the user name of revoked user.
2007 2010 :type userid: str or int
2008 2011
2009 2012 Example error output:
2010 2013
2011 2014 .. code-block:: bash
2012 2015
2013 2016 id : <id_given_in_input>
2014 2017 result: {
2015 2018 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
2016 2019 "success": true
2017 2020 }
2018 2021 error: null
2019 2022 """
2020 2023
2021 2024 repo = get_repo_or_error(repoid)
2022 2025 user = get_user_or_error(userid)
2023 2026 if not has_superadmin_permission(apiuser):
2024 2027 _perms = ('repository.admin',)
2025 2028 validate_repo_permissions(apiuser, repoid, repo, _perms)
2026 2029
2027 2030 perm_deletions = [[user.user_id, None, "user"]]
2028 2031 try:
2029 2032 changes = RepoModel().update_permissions(
2030 2033 repo=repo, perm_deletions=perm_deletions, cur_user=user)
2031 2034
2032 2035 action_data = {
2033 2036 'added': changes['added'],
2034 2037 'updated': changes['updated'],
2035 2038 'deleted': changes['deleted'],
2036 2039 }
2037 2040 audit_logger.store_api(
2038 2041 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2039 2042 Session().commit()
2040 2043 PermissionModel().flush_user_permission_caches(changes)
2041 2044
2042 2045 return {
2043 2046 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
2044 2047 user.username, repo.repo_name
2045 2048 ),
2046 2049 'success': True
2047 2050 }
2048 2051 except Exception:
2049 2052 log.exception("Exception occurred while trying revoke permissions to repo")
2050 2053 raise JSONRPCError(
2051 2054 'failed to edit permission for user: `%s` in repo: `%s`' % (
2052 2055 userid, repoid
2053 2056 )
2054 2057 )
2055 2058
2056 2059
2057 2060 @jsonrpc_method()
2058 2061 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
2059 2062 """
2060 2063 Grant permission for a user group on the specified repository,
2061 2064 or update existing permissions.
2062 2065
2063 2066 This command can only be run using an |authtoken| with admin
2064 2067 permissions on the |repo|.
2065 2068
2066 2069 :param apiuser: This is filled automatically from the |authtoken|.
2067 2070 :type apiuser: AuthUser
2068 2071 :param repoid: Set the repository name or repository ID.
2069 2072 :type repoid: str or int
2070 2073 :param usergroupid: Specify the ID of the user group.
2071 2074 :type usergroupid: str or int
2072 2075 :param perm: Set the user group permissions using the following
2073 2076 format: (repository.(none|read|write|admin))
2074 2077 :type perm: str
2075 2078
2076 2079 Example output:
2077 2080
2078 2081 .. code-block:: bash
2079 2082
2080 2083 id : <id_given_in_input>
2081 2084 result : {
2082 2085 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
2083 2086 "success": true
2084 2087
2085 2088 }
2086 2089 error : null
2087 2090
2088 2091 Example error output:
2089 2092
2090 2093 .. code-block:: bash
2091 2094
2092 2095 id : <id_given_in_input>
2093 2096 result : null
2094 2097 error : {
2095 2098 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
2096 2099 }
2097 2100
2098 2101 """
2099 2102
2100 2103 repo = get_repo_or_error(repoid)
2101 2104 perm = get_perm_or_error(perm)
2102 2105 if not has_superadmin_permission(apiuser):
2103 2106 _perms = ('repository.admin',)
2104 2107 validate_repo_permissions(apiuser, repoid, repo, _perms)
2105 2108
2106 2109 user_group = get_user_group_or_error(usergroupid)
2107 2110 if not has_superadmin_permission(apiuser):
2108 2111 # check if we have at least read permission for this user group !
2109 2112 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2110 2113 if not HasUserGroupPermissionAnyApi(*_perms)(
2111 2114 user=apiuser, user_group_name=user_group.users_group_name):
2112 2115 raise JSONRPCError(
2113 2116 'user group `%s` does not exist' % (usergroupid,))
2114 2117
2115 2118 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
2116 2119 try:
2117 2120 changes = RepoModel().update_permissions(
2118 2121 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
2119 2122 action_data = {
2120 2123 'added': changes['added'],
2121 2124 'updated': changes['updated'],
2122 2125 'deleted': changes['deleted'],
2123 2126 }
2124 2127 audit_logger.store_api(
2125 2128 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2126 2129 Session().commit()
2127 2130 PermissionModel().flush_user_permission_caches(changes)
2128 2131
2129 2132 return {
2130 2133 'msg': 'Granted perm: `%s` for user group: `%s` in '
2131 2134 'repo: `%s`' % (
2132 2135 perm.permission_name, user_group.users_group_name,
2133 2136 repo.repo_name
2134 2137 ),
2135 2138 'success': True
2136 2139 }
2137 2140 except Exception:
2138 2141 log.exception(
2139 2142 "Exception occurred while trying change permission on repo")
2140 2143 raise JSONRPCError(
2141 2144 'failed to edit permission for user group: `%s` in '
2142 2145 'repo: `%s`' % (
2143 2146 usergroupid, repo.repo_name
2144 2147 )
2145 2148 )
2146 2149
2147 2150
2148 2151 @jsonrpc_method()
2149 2152 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
2150 2153 """
2151 2154 Revoke the permissions of a user group on a given repository.
2152 2155
2153 2156 This command can only be run using an |authtoken| with admin
2154 2157 permissions on the |repo|.
2155 2158
2156 2159 :param apiuser: This is filled automatically from the |authtoken|.
2157 2160 :type apiuser: AuthUser
2158 2161 :param repoid: Set the repository name or repository ID.
2159 2162 :type repoid: str or int
2160 2163 :param usergroupid: Specify the user group ID.
2161 2164 :type usergroupid: str or int
2162 2165
2163 2166 Example output:
2164 2167
2165 2168 .. code-block:: bash
2166 2169
2167 2170 id : <id_given_in_input>
2168 2171 result: {
2169 2172 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
2170 2173 "success": true
2171 2174 }
2172 2175 error: null
2173 2176 """
2174 2177
2175 2178 repo = get_repo_or_error(repoid)
2176 2179 if not has_superadmin_permission(apiuser):
2177 2180 _perms = ('repository.admin',)
2178 2181 validate_repo_permissions(apiuser, repoid, repo, _perms)
2179 2182
2180 2183 user_group = get_user_group_or_error(usergroupid)
2181 2184 if not has_superadmin_permission(apiuser):
2182 2185 # check if we have at least read permission for this user group !
2183 2186 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2184 2187 if not HasUserGroupPermissionAnyApi(*_perms)(
2185 2188 user=apiuser, user_group_name=user_group.users_group_name):
2186 2189 raise JSONRPCError(
2187 2190 'user group `%s` does not exist' % (usergroupid,))
2188 2191
2189 2192 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2190 2193 try:
2191 2194 changes = RepoModel().update_permissions(
2192 2195 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2193 2196 action_data = {
2194 2197 'added': changes['added'],
2195 2198 'updated': changes['updated'],
2196 2199 'deleted': changes['deleted'],
2197 2200 }
2198 2201 audit_logger.store_api(
2199 2202 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2200 2203 Session().commit()
2201 2204 PermissionModel().flush_user_permission_caches(changes)
2202 2205
2203 2206 return {
2204 2207 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2205 2208 user_group.users_group_name, repo.repo_name
2206 2209 ),
2207 2210 'success': True
2208 2211 }
2209 2212 except Exception:
2210 2213 log.exception("Exception occurred while trying revoke "
2211 2214 "user group permission on repo")
2212 2215 raise JSONRPCError(
2213 2216 'failed to edit permission for user group: `%s` in '
2214 2217 'repo: `%s`' % (
2215 2218 user_group.users_group_name, repo.repo_name
2216 2219 )
2217 2220 )
2218 2221
2219 2222
2220 2223 @jsonrpc_method()
2221 2224 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2222 2225 """
2223 2226 Triggers a pull on the given repository from a remote location. You
2224 2227 can use this to keep remote repositories up-to-date.
2225 2228
2226 2229 This command can only be run using an |authtoken| with admin
2227 2230 rights to the specified repository. For more information,
2228 2231 see :ref:`config-token-ref`.
2229 2232
2230 2233 This command takes the following options:
2231 2234
2232 2235 :param apiuser: This is filled automatically from the |authtoken|.
2233 2236 :type apiuser: AuthUser
2234 2237 :param repoid: The repository name or repository ID.
2235 2238 :type repoid: str or int
2236 2239 :param remote_uri: Optional remote URI to pass in for pull
2237 2240 :type remote_uri: str
2238 2241
2239 2242 Example output:
2240 2243
2241 2244 .. code-block:: bash
2242 2245
2243 2246 id : <id_given_in_input>
2244 2247 result : {
2245 2248 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2246 2249 "repository": "<repository name>"
2247 2250 }
2248 2251 error : null
2249 2252
2250 2253 Example error output:
2251 2254
2252 2255 .. code-block:: bash
2253 2256
2254 2257 id : <id_given_in_input>
2255 2258 result : null
2256 2259 error : {
2257 2260 "Unable to push changes from `<remote_url>`"
2258 2261 }
2259 2262
2260 2263 """
2261 2264
2262 2265 repo = get_repo_or_error(repoid)
2263 2266 remote_uri = Optional.extract(remote_uri)
2264 2267 remote_uri_display = remote_uri or repo.clone_uri_hidden
2265 2268 if not has_superadmin_permission(apiuser):
2266 2269 _perms = ('repository.admin',)
2267 2270 validate_repo_permissions(apiuser, repoid, repo, _perms)
2268 2271
2269 2272 try:
2270 2273 ScmModel().pull_changes(
2271 2274 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2272 2275 return {
2273 2276 'msg': 'Pulled from url `%s` on repo `%s`' % (
2274 2277 remote_uri_display, repo.repo_name),
2275 2278 'repository': repo.repo_name
2276 2279 }
2277 2280 except Exception:
2278 2281 log.exception("Exception occurred while trying to "
2279 2282 "pull changes from remote location")
2280 2283 raise JSONRPCError(
2281 2284 'Unable to pull changes from `%s`' % remote_uri_display
2282 2285 )
2283 2286
2284 2287
2285 2288 @jsonrpc_method()
2286 2289 def strip(request, apiuser, repoid, revision, branch):
2287 2290 """
2288 2291 Strips the given revision from the specified repository.
2289 2292
2290 2293 * This will remove the revision and all of its decendants.
2291 2294
2292 2295 This command can only be run using an |authtoken| with admin rights to
2293 2296 the specified repository.
2294 2297
2295 2298 This command takes the following options:
2296 2299
2297 2300 :param apiuser: This is filled automatically from the |authtoken|.
2298 2301 :type apiuser: AuthUser
2299 2302 :param repoid: The repository name or repository ID.
2300 2303 :type repoid: str or int
2301 2304 :param revision: The revision you wish to strip.
2302 2305 :type revision: str
2303 2306 :param branch: The branch from which to strip the revision.
2304 2307 :type branch: str
2305 2308
2306 2309 Example output:
2307 2310
2308 2311 .. code-block:: bash
2309 2312
2310 2313 id : <id_given_in_input>
2311 2314 result : {
2312 2315 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2313 2316 "repository": "<repository name>"
2314 2317 }
2315 2318 error : null
2316 2319
2317 2320 Example error output:
2318 2321
2319 2322 .. code-block:: bash
2320 2323
2321 2324 id : <id_given_in_input>
2322 2325 result : null
2323 2326 error : {
2324 2327 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2325 2328 }
2326 2329
2327 2330 """
2328 2331
2329 2332 repo = get_repo_or_error(repoid)
2330 2333 if not has_superadmin_permission(apiuser):
2331 2334 _perms = ('repository.admin',)
2332 2335 validate_repo_permissions(apiuser, repoid, repo, _perms)
2333 2336
2334 2337 try:
2335 2338 ScmModel().strip(repo, revision, branch)
2336 2339 audit_logger.store_api(
2337 2340 'repo.commit.strip', action_data={'commit_id': revision},
2338 2341 repo=repo,
2339 2342 user=apiuser, commit=True)
2340 2343
2341 2344 return {
2342 2345 'msg': 'Stripped commit %s from repo `%s`' % (
2343 2346 revision, repo.repo_name),
2344 2347 'repository': repo.repo_name
2345 2348 }
2346 2349 except Exception:
2347 2350 log.exception("Exception while trying to strip")
2348 2351 raise JSONRPCError(
2349 2352 'Unable to strip commit %s from repo `%s`' % (
2350 2353 revision, repo.repo_name)
2351 2354 )
2352 2355
2353 2356
2354 2357 @jsonrpc_method()
2355 2358 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2356 2359 """
2357 2360 Returns all settings for a repository. If key is given it only returns the
2358 2361 setting identified by the key or null.
2359 2362
2360 2363 :param apiuser: This is filled automatically from the |authtoken|.
2361 2364 :type apiuser: AuthUser
2362 2365 :param repoid: The repository name or repository id.
2363 2366 :type repoid: str or int
2364 2367 :param key: Key of the setting to return.
2365 2368 :type: key: Optional(str)
2366 2369
2367 2370 Example output:
2368 2371
2369 2372 .. code-block:: bash
2370 2373
2371 2374 {
2372 2375 "error": null,
2373 2376 "id": 237,
2374 2377 "result": {
2375 2378 "extensions_largefiles": true,
2376 2379 "extensions_evolve": true,
2377 2380 "hooks_changegroup_push_logger": true,
2378 2381 "hooks_changegroup_repo_size": false,
2379 2382 "hooks_outgoing_pull_logger": true,
2380 2383 "phases_publish": "True",
2381 2384 "rhodecode_hg_use_rebase_for_merging": true,
2382 2385 "rhodecode_pr_merge_enabled": true,
2383 2386 "rhodecode_use_outdated_comments": true
2384 2387 }
2385 2388 }
2386 2389 """
2387 2390
2388 2391 # Restrict access to this api method to super-admins, and repo admins only.
2389 2392 repo = get_repo_or_error(repoid)
2390 2393 if not has_superadmin_permission(apiuser):
2391 2394 _perms = ('repository.admin',)
2392 2395 validate_repo_permissions(apiuser, repoid, repo, _perms)
2393 2396
2394 2397 try:
2395 2398 settings_model = VcsSettingsModel(repo=repo)
2396 2399 settings = settings_model.get_global_settings()
2397 2400 settings.update(settings_model.get_repo_settings())
2398 2401
2399 2402 # If only a single setting is requested fetch it from all settings.
2400 2403 key = Optional.extract(key)
2401 2404 if key is not None:
2402 2405 settings = settings.get(key, None)
2403 2406 except Exception:
2404 2407 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2405 2408 log.exception(msg)
2406 2409 raise JSONRPCError(msg)
2407 2410
2408 2411 return settings
2409 2412
2410 2413
2411 2414 @jsonrpc_method()
2412 2415 def set_repo_settings(request, apiuser, repoid, settings):
2413 2416 """
2414 2417 Update repository settings. Returns true on success.
2415 2418
2416 2419 :param apiuser: This is filled automatically from the |authtoken|.
2417 2420 :type apiuser: AuthUser
2418 2421 :param repoid: The repository name or repository id.
2419 2422 :type repoid: str or int
2420 2423 :param settings: The new settings for the repository.
2421 2424 :type: settings: dict
2422 2425
2423 2426 Example output:
2424 2427
2425 2428 .. code-block:: bash
2426 2429
2427 2430 {
2428 2431 "error": null,
2429 2432 "id": 237,
2430 2433 "result": true
2431 2434 }
2432 2435 """
2433 2436 # Restrict access to this api method to super-admins, and repo admins only.
2434 2437 repo = get_repo_or_error(repoid)
2435 2438 if not has_superadmin_permission(apiuser):
2436 2439 _perms = ('repository.admin',)
2437 2440 validate_repo_permissions(apiuser, repoid, repo, _perms)
2438 2441
2439 2442 if type(settings) is not dict:
2440 2443 raise JSONRPCError('Settings have to be a JSON Object.')
2441 2444
2442 2445 try:
2443 2446 settings_model = VcsSettingsModel(repo=repoid)
2444 2447
2445 2448 # Merge global, repo and incoming settings.
2446 2449 new_settings = settings_model.get_global_settings()
2447 2450 new_settings.update(settings_model.get_repo_settings())
2448 2451 new_settings.update(settings)
2449 2452
2450 2453 # Update the settings.
2451 2454 inherit_global_settings = new_settings.get(
2452 2455 'inherit_global_settings', False)
2453 2456 settings_model.create_or_update_repo_settings(
2454 2457 new_settings, inherit_global_settings=inherit_global_settings)
2455 2458 Session().commit()
2456 2459 except Exception:
2457 2460 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2458 2461 log.exception(msg)
2459 2462 raise JSONRPCError(msg)
2460 2463
2461 2464 # Indicate success.
2462 2465 return True
2463 2466
2464 2467
2465 2468 @jsonrpc_method()
2466 2469 def maintenance(request, apiuser, repoid):
2467 2470 """
2468 2471 Triggers a maintenance on the given repository.
2469 2472
2470 2473 This command can only be run using an |authtoken| with admin
2471 2474 rights to the specified repository. For more information,
2472 2475 see :ref:`config-token-ref`.
2473 2476
2474 2477 This command takes the following options:
2475 2478
2476 2479 :param apiuser: This is filled automatically from the |authtoken|.
2477 2480 :type apiuser: AuthUser
2478 2481 :param repoid: The repository name or repository ID.
2479 2482 :type repoid: str or int
2480 2483
2481 2484 Example output:
2482 2485
2483 2486 .. code-block:: bash
2484 2487
2485 2488 id : <id_given_in_input>
2486 2489 result : {
2487 2490 "msg": "executed maintenance command",
2488 2491 "executed_actions": [
2489 2492 <action_message>, <action_message2>...
2490 2493 ],
2491 2494 "repository": "<repository name>"
2492 2495 }
2493 2496 error : null
2494 2497
2495 2498 Example error output:
2496 2499
2497 2500 .. code-block:: bash
2498 2501
2499 2502 id : <id_given_in_input>
2500 2503 result : null
2501 2504 error : {
2502 2505 "Unable to execute maintenance on `<reponame>`"
2503 2506 }
2504 2507
2505 2508 """
2506 2509
2507 2510 repo = get_repo_or_error(repoid)
2508 2511 if not has_superadmin_permission(apiuser):
2509 2512 _perms = ('repository.admin',)
2510 2513 validate_repo_permissions(apiuser, repoid, repo, _perms)
2511 2514
2512 2515 try:
2513 2516 maintenance = repo_maintenance.RepoMaintenance()
2514 2517 executed_actions = maintenance.execute(repo)
2515 2518
2516 2519 return {
2517 2520 'msg': 'executed maintenance command',
2518 2521 'executed_actions': executed_actions,
2519 2522 'repository': repo.repo_name
2520 2523 }
2521 2524 except Exception:
2522 2525 log.exception("Exception occurred while trying to run maintenance")
2523 2526 raise JSONRPCError(
2524 2527 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,419 +1,419 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 logging
22 22 import itertools
23 23 import base64
24 24
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
27 27
28 28 from rhodecode.api.utils import (
29 29 Optional, OAttr, has_superadmin_permission, get_user_or_error)
30 30 from rhodecode.lib.utils import repo2db_mapper
31 31 from rhodecode.lib import system_info
32 32 from rhodecode.lib import user_sessions
33 33 from rhodecode.lib import exc_tracking
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.utils2 import safe_int
36 36 from rhodecode.model.db import UserIpMap
37 37 from rhodecode.model.scm import ScmModel
38 38 from rhodecode.model.settings import VcsSettingsModel
39 39 from rhodecode.apps.file_store import utils
40 40 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
41 41 FileOverSizeException
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 @jsonrpc_method()
47 47 def get_server_info(request, apiuser):
48 48 """
49 49 Returns the |RCE| server information.
50 50
51 51 This includes the running version of |RCE| and all installed
52 52 packages. This command takes the following options:
53 53
54 54 :param apiuser: This is filled automatically from the |authtoken|.
55 55 :type apiuser: AuthUser
56 56
57 57 Example output:
58 58
59 59 .. code-block:: bash
60 60
61 61 id : <id_given_in_input>
62 62 result : {
63 63 'modules': [<module name>,...]
64 64 'py_version': <python version>,
65 65 'platform': <platform type>,
66 66 'rhodecode_version': <rhodecode version>
67 67 }
68 68 error : null
69 69 """
70 70
71 71 if not has_superadmin_permission(apiuser):
72 72 raise JSONRPCForbidden()
73 73
74 74 server_info = ScmModel().get_server_info(request.environ)
75 75 # rhodecode-index requires those
76 76
77 77 server_info['index_storage'] = server_info['search']['value']['location']
78 78 server_info['storage'] = server_info['storage']['value']['path']
79 79
80 80 return server_info
81 81
82 82
83 83 @jsonrpc_method()
84 84 def get_repo_store(request, apiuser):
85 85 """
86 86 Returns the |RCE| repository storage information.
87 87
88 88 :param apiuser: This is filled automatically from the |authtoken|.
89 89 :type apiuser: AuthUser
90 90
91 91 Example output:
92 92
93 93 .. code-block:: bash
94 94
95 95 id : <id_given_in_input>
96 96 result : {
97 97 'modules': [<module name>,...]
98 98 'py_version': <python version>,
99 99 'platform': <platform type>,
100 100 'rhodecode_version': <rhodecode version>
101 101 }
102 102 error : null
103 103 """
104 104
105 105 if not has_superadmin_permission(apiuser):
106 106 raise JSONRPCForbidden()
107 107
108 108 path = VcsSettingsModel().get_repos_location()
109 109 return {"path": path}
110 110
111 111
112 112 @jsonrpc_method()
113 113 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
114 114 """
115 115 Displays the IP Address as seen from the |RCE| server.
116 116
117 117 * This command displays the IP Address, as well as all the defined IP
118 118 addresses for the specified user. If the ``userid`` is not set, the
119 119 data returned is for the user calling the method.
120 120
121 121 This command can only be run using an |authtoken| with admin rights to
122 122 the specified repository.
123 123
124 124 This command takes the following options:
125 125
126 126 :param apiuser: This is filled automatically from |authtoken|.
127 127 :type apiuser: AuthUser
128 128 :param userid: Sets the userid for which associated IP Address data
129 129 is returned.
130 130 :type userid: Optional(str or int)
131 131
132 132 Example output:
133 133
134 134 .. code-block:: bash
135 135
136 136 id : <id_given_in_input>
137 137 result : {
138 138 "server_ip_addr": "<ip_from_clien>",
139 139 "user_ips": [
140 140 {
141 141 "ip_addr": "<ip_with_mask>",
142 142 "ip_range": ["<start_ip>", "<end_ip>"],
143 143 },
144 144 ...
145 145 ]
146 146 }
147 147
148 148 """
149 149 if not has_superadmin_permission(apiuser):
150 150 raise JSONRPCForbidden()
151 151
152 152 userid = Optional.extract(userid, evaluate_locals=locals())
153 153 userid = getattr(userid, 'user_id', userid)
154 154
155 155 user = get_user_or_error(userid)
156 156 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
157 157 return {
158 158 'server_ip_addr': request.rpc_ip_addr,
159 159 'user_ips': ips
160 160 }
161 161
162 162
163 163 @jsonrpc_method()
164 164 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
165 165 """
166 166 Triggers a rescan of the specified repositories.
167 167
168 168 * If the ``remove_obsolete`` option is set, it also deletes repositories
169 169 that are found in the database but not on the file system, so called
170 170 "clean zombies".
171 171
172 172 This command can only be run using an |authtoken| with admin rights to
173 173 the specified repository.
174 174
175 175 This command takes the following options:
176 176
177 177 :param apiuser: This is filled automatically from the |authtoken|.
178 178 :type apiuser: AuthUser
179 179 :param remove_obsolete: Deletes repositories from the database that
180 180 are not found on the filesystem.
181 181 :type remove_obsolete: Optional(``True`` | ``False``)
182 182
183 183 Example output:
184 184
185 185 .. code-block:: bash
186 186
187 187 id : <id_given_in_input>
188 188 result : {
189 189 'added': [<added repository name>,...]
190 190 'removed': [<removed repository name>,...]
191 191 }
192 192 error : null
193 193
194 194 Example error output:
195 195
196 196 .. code-block:: bash
197 197
198 198 id : <id_given_in_input>
199 199 result : null
200 200 error : {
201 201 'Error occurred during rescan repositories action'
202 202 }
203 203
204 204 """
205 205 if not has_superadmin_permission(apiuser):
206 206 raise JSONRPCForbidden()
207 207
208 208 try:
209 209 rm_obsolete = Optional.extract(remove_obsolete)
210 210 added, removed = repo2db_mapper(ScmModel().repo_scan(),
211 211 remove_obsolete=rm_obsolete)
212 212 return {'added': added, 'removed': removed}
213 213 except Exception:
214 214 log.exception('Failed to run repo rescann')
215 215 raise JSONRPCError(
216 216 'Error occurred during rescan repositories action'
217 217 )
218 218
219 219
220 220 @jsonrpc_method()
221 221 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
222 222 """
223 223 Triggers a session cleanup action.
224 224
225 225 If the ``older_then`` option is set, only sessions that hasn't been
226 226 accessed in the given number of days will be removed.
227 227
228 228 This command can only be run using an |authtoken| with admin rights to
229 229 the specified repository.
230 230
231 231 This command takes the following options:
232 232
233 233 :param apiuser: This is filled automatically from the |authtoken|.
234 234 :type apiuser: AuthUser
235 235 :param older_then: Deletes session that hasn't been accessed
236 236 in given number of days.
237 237 :type older_then: Optional(int)
238 238
239 239 Example output:
240 240
241 241 .. code-block:: bash
242 242
243 243 id : <id_given_in_input>
244 244 result: {
245 245 "backend": "<type of backend>",
246 246 "sessions_removed": <number_of_removed_sessions>
247 247 }
248 248 error : null
249 249
250 250 Example error output:
251 251
252 252 .. code-block:: bash
253 253
254 254 id : <id_given_in_input>
255 255 result : null
256 256 error : {
257 257 'Error occurred during session cleanup'
258 258 }
259 259
260 260 """
261 261 if not has_superadmin_permission(apiuser):
262 262 raise JSONRPCForbidden()
263 263
264 264 older_then = safe_int(Optional.extract(older_then)) or 60
265 265 older_than_seconds = 60 * 60 * 24 * older_then
266 266
267 267 config = system_info.rhodecode_config().get_value()['value']['config']
268 268 session_model = user_sessions.get_session_handler(
269 269 config.get('beaker.session.type', 'memory'))(config)
270 270
271 271 backend = session_model.SESSION_TYPE
272 272 try:
273 273 cleaned = session_model.clean_sessions(
274 274 older_than_seconds=older_than_seconds)
275 275 return {'sessions_removed': cleaned, 'backend': backend}
276 276 except user_sessions.CleanupCommand as msg:
277 277 return {'cleanup_command': msg.message, 'backend': backend}
278 278 except Exception as e:
279 279 log.exception('Failed session cleanup')
280 280 raise JSONRPCError(
281 281 'Error occurred during session cleanup'
282 282 )
283 283
284 284
285 285 @jsonrpc_method()
286 286 def get_method(request, apiuser, pattern=Optional('*')):
287 287 """
288 288 Returns list of all available API methods. By default match pattern
289 289 os "*" but any other pattern can be specified. eg *comment* will return
290 290 all methods with comment inside them. If just single method is matched
291 291 returned data will also include method specification
292 292
293 293 This command can only be run using an |authtoken| with admin rights to
294 294 the specified repository.
295 295
296 296 This command takes the following options:
297 297
298 298 :param apiuser: This is filled automatically from the |authtoken|.
299 299 :type apiuser: AuthUser
300 300 :param pattern: pattern to match method names against
301 301 :type pattern: Optional("*")
302 302
303 303 Example output:
304 304
305 305 .. code-block:: bash
306 306
307 307 id : <id_given_in_input>
308 308 "result": [
309 309 "changeset_comment",
310 310 "comment_pull_request",
311 311 "comment_commit"
312 312 ]
313 313 error : null
314 314
315 315 .. code-block:: bash
316 316
317 317 id : <id_given_in_input>
318 318 "result": [
319 319 "comment_commit",
320 320 {
321 321 "apiuser": "<RequiredType>",
322 322 "comment_type": "<Optional:u'note'>",
323 323 "commit_id": "<RequiredType>",
324 324 "message": "<RequiredType>",
325 325 "repoid": "<RequiredType>",
326 326 "request": "<RequiredType>",
327 327 "resolves_comment_id": "<Optional:None>",
328 328 "status": "<Optional:None>",
329 329 "userid": "<Optional:<OptionalAttr:apiuser>>"
330 330 }
331 331 ]
332 332 error : null
333 333 """
334 334 from rhodecode.config.patches import inspect_getargspec
335 335 inspect = inspect_getargspec()
336 336
337 337 if not has_superadmin_permission(apiuser):
338 338 raise JSONRPCForbidden()
339 339
340 340 pattern = Optional.extract(pattern)
341 341
342 342 matches = find_methods(request.registry.jsonrpc_methods, pattern)
343 343
344 344 args_desc = []
345 345 if len(matches) == 1:
346 346 func = matches[matches.keys()[0]]
347 347
348 348 argspec = inspect.getargspec(func)
349 349 arglist = argspec[0]
350 defaults = map(repr, argspec[3] or [])
350 defaults = list(map(repr, argspec[3] or []))
351 351
352 352 default_empty = '<RequiredType>'
353 353
354 354 # kw arguments required by this method
355 355 func_kwargs = dict(itertools.zip_longest(
356 356 reversed(arglist), reversed(defaults), fillvalue=default_empty))
357 357 args_desc.append(func_kwargs)
358 358
359 359 return matches.keys() + args_desc
360 360
361 361
362 362 @jsonrpc_method()
363 363 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
364 364 """
365 365 Stores sent exception inside the built-in exception tracker in |RCE| server.
366 366
367 367 This command can only be run using an |authtoken| with admin rights to
368 368 the specified repository.
369 369
370 370 This command takes the following options:
371 371
372 372 :param apiuser: This is filled automatically from the |authtoken|.
373 373 :type apiuser: AuthUser
374 374
375 375 :param exc_data_json: JSON data with exception e.g
376 376 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
377 377 :type exc_data_json: JSON data
378 378
379 379 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
380 380 :type prefix: Optional("rhodecode")
381 381
382 382 Example output:
383 383
384 384 .. code-block:: bash
385 385
386 386 id : <id_given_in_input>
387 387 "result": {
388 388 "exc_id": 139718459226384,
389 389 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
390 390 }
391 391 error : null
392 392 """
393 393 if not has_superadmin_permission(apiuser):
394 394 raise JSONRPCForbidden()
395 395
396 396 prefix = Optional.extract(prefix)
397 397 exc_id = exc_tracking.generate_id()
398 398
399 399 try:
400 400 exc_data = json.loads(exc_data_json)
401 401 except Exception:
402 402 log.error('Failed to parse JSON: %r', exc_data_json)
403 403 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
404 404 'Please make sure it contains a valid JSON.')
405 405
406 406 try:
407 407 exc_traceback = exc_data['exc_traceback']
408 408 exc_type_name = exc_data['exc_type_name']
409 409 except KeyError as err:
410 410 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
411 411 'in exc_data_json field. Missing: {}'.format(err))
412 412
413 413 exc_tracking._store_exception(
414 414 exc_id=exc_id, exc_traceback=exc_traceback,
415 415 exc_type_name=exc_type_name, prefix=prefix)
416 416
417 417 exc_url = request.route_url(
418 418 'admin_settings_exception_tracker_show', exception_id=exc_id)
419 419 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,86 +1,93 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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
22 22 import datetime
23 23 import decimal
24 24 import logging
25 25 import time
26 26
27 27 from rhodecode.api import jsonrpc_method, jsonrpc_deprecated_method, JSONRPCError, JSONRPCForbidden
28 28
29 29 from rhodecode.api.utils import Optional, OAttr
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 @jsonrpc_method()
35 35 def test(request, apiuser, args):
36 36 return args
37 37
38 38
39 39 @jsonrpc_method()
40 40 def test_ok(request, apiuser):
41 41 return {
42 'who': u'hello {} '.format(apiuser),
42 'who': f'hello {apiuser}',
43 43 'obj': {
44 44 'time': time.time(),
45 45 'dt': datetime.datetime.now(),
46 46 'decimal': decimal.Decimal('0.123')
47 47 }
48 48 }
49 49
50 50
51 51 @jsonrpc_method()
52 52 def test_error(request, apiuser):
53 53 raise JSONRPCError('error happened')
54 54
55 55
56 56 @jsonrpc_method()
57 57 def test_exception(request, apiuser):
58 raise Exception('something unhanddled')
58 raise Exception('something unhandled')
59 59
60 60
61 61 @jsonrpc_method()
62 62 def test_params(request, apiuser, params):
63 return u'hello apiuser:{} params:{}'.format(apiuser, params)
63 return {
64 'who': f'hello {apiuser}',
65 'params': params
66 }
64 67
65 68
66 69 @jsonrpc_method()
67 70 def test_params_opt(
68 71 request, apiuser, params, opt1=False, opt2=Optional(True),
69 72 opt3=Optional(OAttr('apiuser'))):
70 73 opt2 = Optional.extract(opt2)
71 74 opt3 = Optional.extract(opt3, evaluate_locals=locals())
72
73 return u'hello apiuser:{} params:{}, opt:[{},{},{}]'.format(
74 apiuser, params, opt1, opt2, opt3)
75 return {
76 'who': f'hello {apiuser}',
77 'params': params,
78 'opts': [
79 opt1, opt2, opt3
80 ]
81 }
75 82
76 83
77 84 @jsonrpc_method()
78 85 @jsonrpc_deprecated_method(
79 86 use_method='test_ok', deprecated_at_version='4.0.0')
80 87 def test_deprecated_method(request, apiuser):
81 return u'value'
88 return 'value'
82 89
83 90
84 91 @jsonrpc_method()
85 92 def test_forbidden_method(request, apiuser):
86 93 raise JSONRPCForbidden()
General Comments 0
You need to be logged in to leave comments. Login now