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