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