##// 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 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import itertools
19 import itertools
22 import logging
20 import logging
23 import sys
21 import sys
24 import fnmatch
22 import fnmatch
25
23
26 import decorator
24 import decorator
27 import typing
25 import typing
28 import venusian
26 import venusian
29 from collections import OrderedDict
27 from collections import OrderedDict
30
28
31 from pyramid.exceptions import ConfigurationError
29 from pyramid.exceptions import ConfigurationError
32 from pyramid.renderers import render
30 from pyramid.renderers import render
33 from pyramid.response import Response
31 from pyramid.response import Response
34 from pyramid.httpexceptions import HTTPNotFound
32 from pyramid.httpexceptions import HTTPNotFound
35
33
36 from rhodecode.api.exc import (
34 from rhodecode.api.exc import (
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
35 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 from rhodecode.apps._base import TemplateArgs
36 from rhodecode.apps._base import TemplateArgs
39 from rhodecode.lib.auth import AuthUser
37 from rhodecode.lib.auth import AuthUser
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
38 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
41 from rhodecode.lib.exc_tracking import store_exception
39 from rhodecode.lib.exc_tracking import store_exception
42 from rhodecode.lib import ext_json
40 from rhodecode.lib import ext_json
43 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.utils2 import safe_str
44 from rhodecode.lib.plugins.utils import get_plugin_settings
42 from rhodecode.lib.plugins.utils import get_plugin_settings
45 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.model.db import User, UserApiKeys
46
44
47 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
48
46
49 DEFAULT_RENDERER = 'jsonrpc_renderer'
47 DEFAULT_RENDERER = 'jsonrpc_renderer'
50 DEFAULT_URL = '/_admin/apiv2'
48 DEFAULT_URL = '/_admin/apiv2'
51
49
52
50
53 def find_methods(jsonrpc_methods, pattern):
51 def find_methods(jsonrpc_methods, pattern):
54 matches = OrderedDict()
52 matches = OrderedDict()
55 if not isinstance(pattern, (list, tuple)):
53 if not isinstance(pattern, (list, tuple)):
56 pattern = [pattern]
54 pattern = [pattern]
57
55
58 for single_pattern in pattern:
56 for single_pattern in pattern:
59 for method_name, method in jsonrpc_methods.items():
57 for method_name, method in jsonrpc_methods.items():
60 if fnmatch.fnmatch(method_name, single_pattern):
58 if fnmatch.fnmatch(method_name, single_pattern):
61 matches[method_name] = method
59 matches[method_name] = method
62 return matches
60 return matches
63
61
64
62
65 class ExtJsonRenderer(object):
63 class ExtJsonRenderer(object):
66 """
64 """
67 Custom renderer that makes use of our ext_json lib
65 Custom renderer that makes use of our ext_json lib
68
66
69 """
67 """
70
68
71 def __init__(self):
69 def __init__(self):
72 self.serializer = ext_json.formatted_json
70 self.serializer = ext_json.formatted_json
73
71
74 def __call__(self, info):
72 def __call__(self, info):
75 """ Returns a plain JSON-encoded string with content-type
73 """ Returns a plain JSON-encoded string with content-type
76 ``application/json``. The content-type may be overridden by
74 ``application/json``. The content-type may be overridden by
77 setting ``request.response.content_type``."""
75 setting ``request.response.content_type``."""
78
76
79 def _render(value, system):
77 def _render(value, system):
80 request = system.get('request')
78 request = system.get('request')
81 if request is not None:
79 if request is not None:
82 response = request.response
80 response = request.response
83 ct = response.content_type
81 ct = response.content_type
84 if ct == response.default_content_type:
82 if ct == response.default_content_type:
85 response.content_type = 'application/json'
83 response.content_type = 'application/json'
86
84
87 return self.serializer(value)
85 return self.serializer(value)
88
86
89 return _render
87 return _render
90
88
91
89
92 def jsonrpc_response(request, result):
90 def jsonrpc_response(request, result):
93 rpc_id = getattr(request, 'rpc_id', None)
91 rpc_id = getattr(request, 'rpc_id', None)
94
92
95 ret_value = ''
93 ret_value = ''
96 if rpc_id:
94 if rpc_id:
97 ret_value = {'id': rpc_id, 'result': result, 'error': None}
95 ret_value = {'id': rpc_id, 'result': result, 'error': None}
98
96
99 # fetch deprecation warnings, and store it inside results
97 # fetch deprecation warnings, and store it inside results
100 deprecation = getattr(request, 'rpc_deprecation', None)
98 deprecation = getattr(request, 'rpc_deprecation', None)
101 if deprecation:
99 if deprecation:
102 ret_value['DEPRECATION_WARNING'] = deprecation
100 ret_value['DEPRECATION_WARNING'] = deprecation
103
101
104 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
102 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
105 content_type = 'application/json'
103 content_type = 'application/json'
106 content_type_header = 'Content-Type'
104 content_type_header = 'Content-Type'
107 headers = {
105 headers = {
108 content_type_header: content_type
106 content_type_header: content_type
109 }
107 }
110 return Response(
108 return Response(
111 body=raw_body,
109 body=raw_body,
112 content_type=content_type,
110 content_type=content_type,
113 headerlist=[(k, v) for k, v in headers.items()]
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 Generate a Response object with a JSON-RPC error body
117 Generate a Response object with a JSON-RPC error body
120 """
118 """
121 headers = headers or {}
119 headers = headers or {}
122 content_type = 'application/json'
120 content_type = 'application/json'
123 content_type_header = 'Content-Type'
121 content_type_header = 'Content-Type'
124 if content_type_header not in headers:
122 if content_type_header not in headers:
125 headers[content_type_header] = content_type
123 headers[content_type_header] = content_type
126
124
127 err_dict = {'id': retid, 'result': None, 'error': message}
125 err_dict = {'id': retid, 'result': None, 'error': message}
128 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
126 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
129
127
130 return Response(
128 return Response(
131 body=raw_body,
129 body=raw_body,
132 status=code,
130 status=code,
133 content_type=content_type,
131 content_type=content_type,
134 headerlist=[(k, v) for k, v in headers.items()]
132 headerlist=[(k, v) for k, v in headers.items()]
135 )
133 )
136
134
137
135
138 def exception_view(exc, request):
136 def exception_view(exc, request):
139 rpc_id = getattr(request, 'rpc_id', None)
137 rpc_id = getattr(request, 'rpc_id', None)
140
138
141 if isinstance(exc, JSONRPCError):
139 if isinstance(exc, JSONRPCError):
142 fault_message = safe_str(exc.message)
140 fault_message = safe_str(exc.message)
143 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
141 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
144 elif isinstance(exc, JSONRPCValidationError):
142 elif isinstance(exc, JSONRPCValidationError):
145 colander_exc = exc.colander_exception
143 colander_exc = exc.colander_exception
146 # TODO(marcink): think maybe of nicer way to serialize errors ?
144 # TODO(marcink): think maybe of nicer way to serialize errors ?
147 fault_message = colander_exc.asdict()
145 fault_message = colander_exc.asdict()
148 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
146 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
149 elif isinstance(exc, JSONRPCForbidden):
147 elif isinstance(exc, JSONRPCForbidden):
150 fault_message = 'Access was denied to this resource.'
148 fault_message = 'Access was denied to this resource.'
151 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
149 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
152 elif isinstance(exc, HTTPNotFound):
150 elif isinstance(exc, HTTPNotFound):
153 method = request.rpc_method
151 method = request.rpc_method
154 log.debug('json-rpc method `%s` not found in list of '
152 log.debug('json-rpc method `%s` not found in list of '
155 'api calls: %s, rpc_id:%s',
153 'api calls: %s, rpc_id:%s',
156 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
154 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
157
155
158 similar = 'none'
156 similar = 'none'
159 try:
157 try:
160 similar_paterns = [f'*{x}*' for x in method.split('_')]
158 similar_paterns = [f'*{x}*' for x in method.split('_')]
161 similar_found = find_methods(
159 similar_found = find_methods(
162 request.registry.jsonrpc_methods, similar_paterns)
160 request.registry.jsonrpc_methods, similar_paterns)
163 similar = ', '.join(similar_found.keys()) or similar
161 similar = ', '.join(similar_found.keys()) or similar
164 except Exception:
162 except Exception:
165 # make the whole above block safe
163 # make the whole above block safe
166 pass
164 pass
167
165
168 fault_message = "No such method: {}. Similar methods: {}".format(
166 fault_message = "No such method: {}. Similar methods: {}".format(
169 method, similar)
167 method, similar)
170 else:
168 else:
171 fault_message = 'undefined error'
169 fault_message = 'undefined error'
172 exc_info = exc.exc_info()
170 exc_info = exc.exc_info()
173 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
171 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
174
172
175 statsd = request.registry.statsd
173 statsd = request.registry.statsd
176 if statsd:
174 if statsd:
177 exc_type = "{}.{}".format(exc.__class__.__module__, exc.__class__.__name__)
175 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
178 statsd.incr('rhodecode_exception_total',
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 return jsonrpc_error(request, fault_message, rpc_id)
179 return jsonrpc_error(request, fault_message, rpc_id)
182
180
183
181
184 def request_view(request):
182 def request_view(request):
185 """
183 """
186 Main request handling method. It handles all logic to call a specific
184 Main request handling method. It handles all logic to call a specific
187 exposed method
185 exposed method
188 """
186 """
189 # cython compatible inspect
187 # cython compatible inspect
190 from rhodecode.config.patches import inspect_getargspec
188 from rhodecode.config.patches import inspect_getargspec
191 inspect = inspect_getargspec()
189 inspect = inspect_getargspec()
192
190
193 # check if we can find this session using api_key, get_by_auth_token
191 # check if we can find this session using api_key, get_by_auth_token
194 # search not expired tokens only
192 # search not expired tokens only
195 try:
193 try:
196 api_user = User.get_by_auth_token(request.rpc_api_key)
194 api_user = User.get_by_auth_token(request.rpc_api_key)
197
195
198 if api_user is None:
196 if api_user is None:
199 return jsonrpc_error(
197 return jsonrpc_error(
200 request, retid=request.rpc_id, message='Invalid API KEY')
198 request, retid=request.rpc_id, message='Invalid API KEY')
201
199
202 if not api_user.active:
200 if not api_user.active:
203 return jsonrpc_error(
201 return jsonrpc_error(
204 request, retid=request.rpc_id,
202 request, retid=request.rpc_id,
205 message='Request from this user not allowed')
203 message='Request from this user not allowed')
206
204
207 # check if we are allowed to use this IP
205 # check if we are allowed to use this IP
208 auth_u = AuthUser(
206 auth_u = AuthUser(
209 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
207 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
210 if not auth_u.ip_allowed:
208 if not auth_u.ip_allowed:
211 return jsonrpc_error(
209 return jsonrpc_error(
212 request, retid=request.rpc_id,
210 request, retid=request.rpc_id,
213 message='Request from IP:%s not allowed' % (
211 message='Request from IP:{} not allowed'.format(
214 request.rpc_ip_addr,))
212 request.rpc_ip_addr))
215 else:
213 else:
216 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
214 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
217
215
218 # register our auth-user
216 # register our auth-user
219 request.rpc_user = auth_u
217 request.rpc_user = auth_u
220 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
218 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
221
219
222 # now check if token is valid for API
220 # now check if token is valid for API
223 auth_token = request.rpc_api_key
221 auth_token = request.rpc_api_key
224 token_match = api_user.authenticate_by_token(
222 token_match = api_user.authenticate_by_token(
225 auth_token, roles=[UserApiKeys.ROLE_API])
223 auth_token, roles=[UserApiKeys.ROLE_API])
226 invalid_token = not token_match
224 invalid_token = not token_match
227
225
228 log.debug('Checking if API KEY is valid with proper role')
226 log.debug('Checking if API KEY is valid with proper role')
229 if invalid_token:
227 if invalid_token:
230 return jsonrpc_error(
228 return jsonrpc_error(
231 request, retid=request.rpc_id,
229 request, retid=request.rpc_id,
232 message='API KEY invalid or, has bad role for an API call')
230 message='API KEY invalid or, has bad role for an API call')
233
231
234 except Exception:
232 except Exception:
235 log.exception('Error on API AUTH')
233 log.exception('Error on API AUTH')
236 return jsonrpc_error(
234 return jsonrpc_error(
237 request, retid=request.rpc_id, message='Invalid API KEY')
235 request, retid=request.rpc_id, message='Invalid API KEY')
238
236
239 method = request.rpc_method
237 method = request.rpc_method
240 func = request.registry.jsonrpc_methods[method]
238 func = request.registry.jsonrpc_methods[method]
241
239
242 # now that we have a method, add request._req_params to
240 # now that we have a method, add request._req_params to
243 # self.kargs and dispatch control to WGIController
241 # self.kargs and dispatch control to WGIController
244
242
245 argspec = inspect.getargspec(func)
243 argspec = inspect.getargspec(func)
246 arglist = argspec[0]
244 arglist = argspec[0]
247 defs = argspec[3] or []
245 defs = argspec[3] or []
248 defaults = [type(a) for a in defs]
246 defaults = [type(a) for a in defs]
249 default_empty = type(NotImplemented)
247 default_empty = type(NotImplemented)
250
248
251 # kw arguments required by this method
249 # kw arguments required by this method
252 func_kwargs = dict(itertools.zip_longest(
250 func_kwargs = dict(itertools.zip_longest(
253 reversed(arglist), reversed(defaults), fillvalue=default_empty))
251 reversed(arglist), reversed(defaults), fillvalue=default_empty))
254
252
255 # This attribute will need to be first param of a method that uses
253 # This attribute will need to be first param of a method that uses
256 # api_key, which is translated to instance of user at that name
254 # api_key, which is translated to instance of user at that name
257 user_var = 'apiuser'
255 user_var = 'apiuser'
258 request_var = 'request'
256 request_var = 'request'
259
257
260 for arg in [user_var, request_var]:
258 for arg in [user_var, request_var]:
261 if arg not in arglist:
259 if arg not in arglist:
262 return jsonrpc_error(
260 return jsonrpc_error(
263 request,
261 request,
264 retid=request.rpc_id,
262 retid=request.rpc_id,
265 message='This method [%s] does not support '
263 message='This method [%s] does not support '
266 'required parameter `%s`' % (func.__name__, arg))
264 'required parameter `%s`' % (func.__name__, arg))
267
265
268 # get our arglist and check if we provided them as args
266 # get our arglist and check if we provided them as args
269 for arg, default in func_kwargs.items():
267 for arg, default in func_kwargs.items():
270 if arg in [user_var, request_var]:
268 if arg in [user_var, request_var]:
271 # user_var and request_var are pre-hardcoded parameters and we
269 # user_var and request_var are pre-hardcoded parameters and we
272 # don't need to do any translation
270 # don't need to do any translation
273 continue
271 continue
274
272
275 # skip the required param check if it's default value is
273 # skip the required param check if it's default value is
276 # NotImplementedType (default_empty)
274 # NotImplementedType (default_empty)
277 if default == default_empty and arg not in request.rpc_params:
275 if default == default_empty and arg not in request.rpc_params:
278 return jsonrpc_error(
276 return jsonrpc_error(
279 request,
277 request,
280 retid=request.rpc_id,
278 retid=request.rpc_id,
281 message=('Missing non optional `%s` arg in JSON DATA' % arg)
279 message=('Missing non optional `%s` arg in JSON DATA' % arg)
282 )
280 )
283
281
284 # sanitize extra passed arguments
282 # sanitize extra passed arguments
285 for k in list(request.rpc_params.keys()):
283 for k in list(request.rpc_params.keys()):
286 if k not in func_kwargs:
284 if k not in func_kwargs:
287 del request.rpc_params[k]
285 del request.rpc_params[k]
288
286
289 call_params = request.rpc_params
287 call_params = request.rpc_params
290 call_params.update({
288 call_params.update({
291 'request': request,
289 'request': request,
292 'apiuser': auth_u
290 'apiuser': auth_u
293 })
291 })
294
292
295 # register some common functions for usage
293 # register some common functions for usage
296 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
294 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
297
295
298 statsd = request.registry.statsd
296 statsd = request.registry.statsd
299
297
300 try:
298 try:
301 ret_value = func(**call_params)
299 ret_value = func(**call_params)
302 resp = jsonrpc_response(request, ret_value)
300 resp = jsonrpc_response(request, ret_value)
303 if statsd:
301 if statsd:
304 statsd.incr('rhodecode_api_call_success_total')
302 statsd.incr('rhodecode_api_call_success_total')
305 return resp
303 return resp
306 except JSONRPCBaseError:
304 except JSONRPCBaseError:
307 raise
305 raise
308 except Exception:
306 except Exception:
309 log.exception('Unhandled exception occurred on api call: %s', func)
307 log.exception('Unhandled exception occurred on api call: %s', func)
310 exc_info = sys.exc_info()
308 exc_info = sys.exc_info()
311 exc_id, exc_type_name = store_exception(
309 exc_id, exc_type_name = store_exception(
312 id(exc_info), exc_info, prefix='rhodecode-api')
310 id(exc_info), exc_info, prefix='rhodecode-api')
313 error_headers = {
311 error_headers = {
314 'RhodeCode-Exception-Id': str(exc_id),
312 'RhodeCode-Exception-Id': str(exc_id),
315 'RhodeCode-Exception-Type': str(exc_type_name)
313 'RhodeCode-Exception-Type': str(exc_type_name)
316 }
314 }
317 err_resp = jsonrpc_error(
315 err_resp = jsonrpc_error(
318 request, retid=request.rpc_id, message='Internal server error',
316 request, retid=request.rpc_id, message='Internal server error',
319 headers=error_headers)
317 headers=error_headers)
320 if statsd:
318 if statsd:
321 statsd.incr('rhodecode_api_call_fail_total')
319 statsd.incr('rhodecode_api_call_fail_total')
322 return err_resp
320 return err_resp
323
321
324
322
325 def setup_request(request):
323 def setup_request(request):
326 """
324 """
327 Parse a JSON-RPC request body. It's used inside the predicates method
325 Parse a JSON-RPC request body. It's used inside the predicates method
328 to validate and bootstrap requests for usage in rpc calls.
326 to validate and bootstrap requests for usage in rpc calls.
329
327
330 We need to raise JSONRPCError here if we want to return some errors back to
328 We need to raise JSONRPCError here if we want to return some errors back to
331 user.
329 user.
332 """
330 """
333
331
334 log.debug('Executing setup request: %r', request)
332 log.debug('Executing setup request: %r', request)
335 request.rpc_ip_addr = get_ip_addr(request.environ)
333 request.rpc_ip_addr = get_ip_addr(request.environ)
336 # TODO(marcink): deprecate GET at some point
334 # TODO(marcink): deprecate GET at some point
337 if request.method not in ['POST', 'GET']:
335 if request.method not in ['POST', 'GET']:
338 log.debug('unsupported request method "%s"', request.method)
336 log.debug('unsupported request method "%s"', request.method)
339 raise JSONRPCError(
337 raise JSONRPCError(
340 'unsupported request method "%s". Please use POST' % request.method)
338 'unsupported request method "%s". Please use POST' % request.method)
341
339
342 if 'CONTENT_LENGTH' not in request.environ:
340 if 'CONTENT_LENGTH' not in request.environ:
343 log.debug("No Content-Length")
341 log.debug("No Content-Length")
344 raise JSONRPCError("Empty body, No Content-Length in request")
342 raise JSONRPCError("Empty body, No Content-Length in request")
345
343
346 else:
344 else:
347 length = request.environ['CONTENT_LENGTH']
345 length = request.environ['CONTENT_LENGTH']
348 log.debug('Content-Length: %s', length)
346 log.debug('Content-Length: %s', length)
349
347
350 if length == 0:
348 if length == 0:
351 log.debug("Content-Length is 0")
349 log.debug("Content-Length is 0")
352 raise JSONRPCError("Content-Length is 0")
350 raise JSONRPCError("Content-Length is 0")
353
351
354 raw_body = request.body
352 raw_body = request.body
355 log.debug("Loading JSON body now")
353 log.debug("Loading JSON body now")
356 try:
354 try:
357 json_body = ext_json.json.loads(raw_body)
355 json_body = ext_json.json.loads(raw_body)
358 except ValueError as e:
356 except ValueError as e:
359 # catch JSON errors Here
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 request.rpc_id = json_body.get('id')
360 request.rpc_id = json_body.get('id')
363 request.rpc_method = json_body.get('method')
361 request.rpc_method = json_body.get('method')
364
362
365 # check required base parameters
363 # check required base parameters
366 try:
364 try:
367 api_key = json_body.get('api_key')
365 api_key = json_body.get('api_key')
368 if not api_key:
366 if not api_key:
369 api_key = json_body.get('auth_token')
367 api_key = json_body.get('auth_token')
370
368
371 if not api_key:
369 if not api_key:
372 raise KeyError('api_key or auth_token')
370 raise KeyError('api_key or auth_token')
373
371
374 # TODO(marcink): support passing in token in request header
372 # TODO(marcink): support passing in token in request header
375
373
376 request.rpc_api_key = api_key
374 request.rpc_api_key = api_key
377 request.rpc_id = json_body['id']
375 request.rpc_id = json_body['id']
378 request.rpc_method = json_body['method']
376 request.rpc_method = json_body['method']
379 request.rpc_params = json_body['args'] \
377 request.rpc_params = json_body['args'] \
380 if isinstance(json_body['args'], dict) else {}
378 if isinstance(json_body['args'], dict) else {}
381
379
382 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
380 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
383 except KeyError as e:
381 except KeyError as e:
384 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
382 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
385
383
386 log.debug('setup complete, now handling method:%s rpcid:%s',
384 log.debug('setup complete, now handling method:%s rpcid:%s',
387 request.rpc_method, request.rpc_id, )
385 request.rpc_method, request.rpc_id, )
388
386
389
387
390 class RoutePredicate(object):
388 class RoutePredicate(object):
391 def __init__(self, val, config):
389 def __init__(self, val, config):
392 self.val = val
390 self.val = val
393
391
394 def text(self):
392 def text(self):
395 return f'jsonrpc route = {self.val}'
393 return f'jsonrpc route = {self.val}'
396
394
397 phash = text
395 phash = text
398
396
399 def __call__(self, info, request):
397 def __call__(self, info, request):
400 if self.val:
398 if self.val:
401 # potentially setup and bootstrap our call
399 # potentially setup and bootstrap our call
402 setup_request(request)
400 setup_request(request)
403
401
404 # Always return True so that even if it isn't a valid RPC it
402 # Always return True so that even if it isn't a valid RPC it
405 # will fall through to the underlaying handlers like notfound_view
403 # will fall through to the underlaying handlers like notfound_view
406 return True
404 return True
407
405
408
406
409 class NotFoundPredicate(object):
407 class NotFoundPredicate(object):
410 def __init__(self, val, config):
408 def __init__(self, val, config):
411 self.val = val
409 self.val = val
412 self.methods = config.registry.jsonrpc_methods
410 self.methods = config.registry.jsonrpc_methods
413
411
414 def text(self):
412 def text(self):
415 return f'jsonrpc method not found = {self.val}'
413 return f'jsonrpc method not found = {self.val}'
416
414
417 phash = text
415 phash = text
418
416
419 def __call__(self, info, request):
417 def __call__(self, info, request):
420 return hasattr(request, 'rpc_method')
418 return hasattr(request, 'rpc_method')
421
419
422
420
423 class MethodPredicate(object):
421 class MethodPredicate(object):
424 def __init__(self, val, config):
422 def __init__(self, val, config):
425 self.method = val
423 self.method = val
426
424
427 def text(self):
425 def text(self):
428 return f'jsonrpc method = {self.method}'
426 return f'jsonrpc method = {self.method}'
429
427
430 phash = text
428 phash = text
431
429
432 def __call__(self, context, request):
430 def __call__(self, context, request):
433 # we need to explicitly return False here, so pyramid doesn't try to
431 # we need to explicitly return False here, so pyramid doesn't try to
434 # execute our view directly. We need our main handler to execute things
432 # execute our view directly. We need our main handler to execute things
435 return getattr(request, 'rpc_method') == self.method
433 return getattr(request, 'rpc_method') == self.method
436
434
437
435
438 def add_jsonrpc_method(config, view, **kwargs):
436 def add_jsonrpc_method(config, view, **kwargs):
439 # pop the method name
437 # pop the method name
440 method = kwargs.pop('method', None)
438 method = kwargs.pop('method', None)
441
439
442 if method is None:
440 if method is None:
443 raise ConfigurationError(
441 raise ConfigurationError(
444 'Cannot register a JSON-RPC method without specifying the "method"')
442 'Cannot register a JSON-RPC method without specifying the "method"')
445
443
446 # we define custom predicate, to enable to detect conflicting methods,
444 # we define custom predicate, to enable to detect conflicting methods,
447 # those predicates are kind of "translation" from the decorator variables
445 # those predicates are kind of "translation" from the decorator variables
448 # to internal predicates names
446 # to internal predicates names
449
447
450 kwargs['jsonrpc_method'] = method
448 kwargs['jsonrpc_method'] = method
451
449
452 # register our view into global view store for validation
450 # register our view into global view store for validation
453 config.registry.jsonrpc_methods[method] = view
451 config.registry.jsonrpc_methods[method] = view
454
452
455 # we're using our main request_view handler, here, so each method
453 # we're using our main request_view handler, here, so each method
456 # has a unified handler for itself
454 # has a unified handler for itself
457 config.add_view(request_view, route_name='apiv2', **kwargs)
455 config.add_view(request_view, route_name='apiv2', **kwargs)
458
456
459
457
460 class jsonrpc_method(object):
458 class jsonrpc_method(object):
461 """
459 """
462 decorator that works similar to @add_view_config decorator,
460 decorator that works similar to @add_view_config decorator,
463 but tailored for our JSON RPC
461 but tailored for our JSON RPC
464 """
462 """
465
463
466 venusian = venusian # for testing injection
464 venusian = venusian # for testing injection
467
465
468 def __init__(self, method=None, **kwargs):
466 def __init__(self, method=None, **kwargs):
469 self.method = method
467 self.method = method
470 self.kwargs = kwargs
468 self.kwargs = kwargs
471
469
472 def __call__(self, wrapped):
470 def __call__(self, wrapped):
473 kwargs = self.kwargs.copy()
471 kwargs = self.kwargs.copy()
474 kwargs['method'] = self.method or wrapped.__name__
472 kwargs['method'] = self.method or wrapped.__name__
475 depth = kwargs.pop('_depth', 0)
473 depth = kwargs.pop('_depth', 0)
476
474
477 def callback(context, name, ob):
475 def callback(context, name, ob):
478 config = context.config.with_package(info.module)
476 config = context.config.with_package(info.module)
479 config.add_jsonrpc_method(view=ob, **kwargs)
477 config.add_jsonrpc_method(view=ob, **kwargs)
480
478
481 info = venusian.attach(wrapped, callback, category='pyramid',
479 info = venusian.attach(wrapped, callback, category='pyramid',
482 depth=depth + 1)
480 depth=depth + 1)
483 if info.scope == 'class':
481 if info.scope == 'class':
484 # ensure that attr is set if decorating a class method
482 # ensure that attr is set if decorating a class method
485 kwargs.setdefault('attr', wrapped.__name__)
483 kwargs.setdefault('attr', wrapped.__name__)
486
484
487 kwargs['_info'] = info.codeinfo # fbo action_method
485 kwargs['_info'] = info.codeinfo # fbo action_method
488 return wrapped
486 return wrapped
489
487
490
488
491 class jsonrpc_deprecated_method(object):
489 class jsonrpc_deprecated_method(object):
492 """
490 """
493 Marks method as deprecated, adds log.warning, and inject special key to
491 Marks method as deprecated, adds log.warning, and inject special key to
494 the request variable to mark method as deprecated.
492 the request variable to mark method as deprecated.
495 Also injects special docstring that extract_docs will catch to mark
493 Also injects special docstring that extract_docs will catch to mark
496 method as deprecated.
494 method as deprecated.
497
495
498 :param use_method: specify which method should be used instead of
496 :param use_method: specify which method should be used instead of
499 the decorated one
497 the decorated one
500
498
501 Use like::
499 Use like::
502
500
503 @jsonrpc_method()
501 @jsonrpc_method()
504 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
502 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
505 def old_func(request, apiuser, arg1, arg2):
503 def old_func(request, apiuser, arg1, arg2):
506 ...
504 ...
507 """
505 """
508
506
509 def __init__(self, use_method, deprecated_at_version):
507 def __init__(self, use_method, deprecated_at_version):
510 self.use_method = use_method
508 self.use_method = use_method
511 self.deprecated_at_version = deprecated_at_version
509 self.deprecated_at_version = deprecated_at_version
512 self.deprecated_msg = ''
510 self.deprecated_msg = ''
513
511
514 def __call__(self, func):
512 def __call__(self, func):
515 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
513 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
516 method=self.use_method)
514 method=self.use_method)
517
515
518 docstring = """\n
516 docstring = """\n
519 .. deprecated:: {version}
517 .. deprecated:: {version}
520
518
521 {deprecation_message}
519 {deprecation_message}
522
520
523 {original_docstring}
521 {original_docstring}
524 """
522 """
525 func.__doc__ = docstring.format(
523 func.__doc__ = docstring.format(
526 version=self.deprecated_at_version,
524 version=self.deprecated_at_version,
527 deprecation_message=self.deprecated_msg,
525 deprecation_message=self.deprecated_msg,
528 original_docstring=func.__doc__)
526 original_docstring=func.__doc__)
529 return decorator.decorator(self.__wrapper, func)
527 return decorator.decorator(self.__wrapper, func)
530
528
531 def __wrapper(self, func, *fargs, **fkwargs):
529 def __wrapper(self, func, *fargs, **fkwargs):
532 log.warning('DEPRECATED API CALL on function %s, please '
530 log.warning('DEPRECATED API CALL on function %s, please '
533 'use `%s` instead', func, self.use_method)
531 'use `%s` instead', func, self.use_method)
534 # alter function docstring to mark as deprecated, this is picked up
532 # alter function docstring to mark as deprecated, this is picked up
535 # via fabric file that generates API DOC.
533 # via fabric file that generates API DOC.
536 result = func(*fargs, **fkwargs)
534 result = func(*fargs, **fkwargs)
537
535
538 request = fargs[0]
536 request = fargs[0]
539 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
537 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
540 return result
538 return result
541
539
542
540
543 def add_api_methods(config):
541 def add_api_methods(config):
544 from rhodecode.api.views import (
542 from rhodecode.api.views import (
545 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
543 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
546 server_api, search_api, testing_api, user_api, user_group_api)
544 server_api, search_api, testing_api, user_api, user_group_api)
547
545
548 config.scan('rhodecode.api.views')
546 config.scan('rhodecode.api.views')
549
547
550
548
551 def includeme(config):
549 def includeme(config):
552 plugin_module = 'rhodecode.api'
550 plugin_module = 'rhodecode.api'
553 plugin_settings = get_plugin_settings(
551 plugin_settings = get_plugin_settings(
554 plugin_module, config.registry.settings)
552 plugin_module, config.registry.settings)
555
553
556 if not hasattr(config.registry, 'jsonrpc_methods'):
554 if not hasattr(config.registry, 'jsonrpc_methods'):
557 config.registry.jsonrpc_methods = OrderedDict()
555 config.registry.jsonrpc_methods = OrderedDict()
558
556
559 # match filter by given method only
557 # match filter by given method only
560 config.add_view_predicate('jsonrpc_method', MethodPredicate)
558 config.add_view_predicate('jsonrpc_method', MethodPredicate)
561 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
559 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
562
560
563 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
561 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
564 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
562 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
565
563
566 config.add_route_predicate(
564 config.add_route_predicate(
567 'jsonrpc_call', RoutePredicate)
565 'jsonrpc_call', RoutePredicate)
568
566
569 config.add_route(
567 config.add_route(
570 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
568 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
571
569
572 # register some exception handling view
570 # register some exception handling view
573 config.add_view(exception_view, context=JSONRPCBaseError)
571 config.add_view(exception_view, context=JSONRPCBaseError)
574 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
572 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
575
573
576 add_api_methods(config)
574 add_api_methods(config)
@@ -1,42 +1,40 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 class JSONRPCBaseError(Exception):
20 class JSONRPCBaseError(Exception):
23 def __init__(self, message='', *args):
21 def __init__(self, message='', *args):
24 self.message = message
22 self.message = message
25 super(JSONRPCBaseError, self).__init__(message, *args)
23 super().__init__(message, *args)
26
24
27
25
28 class JSONRPCError(JSONRPCBaseError):
26 class JSONRPCError(JSONRPCBaseError):
29 pass
27 pass
30
28
31
29
32 class JSONRPCValidationError(JSONRPCBaseError):
30 class JSONRPCValidationError(JSONRPCBaseError):
33
31
34 def __init__(self, *args, **kwargs):
32 def __init__(self, *args, **kwargs):
35 self.colander_exception = kwargs.pop('colander_exc')
33 self.colander_exception = kwargs.pop('colander_exc')
36 super(JSONRPCValidationError, self).__init__(
34 super().__init__(
37 message=self.colander_exception, *args)
35 message=self.colander_exception, *args)
38
36
39
37
40 class JSONRPCForbidden(JSONRPCBaseError):
38 class JSONRPCForbidden(JSONRPCBaseError):
41 pass
39 pass
42
40
@@ -1,458 +1,456 b''
1
2
3 # Copyright (C) 2014-2023 RhodeCode GmbH
1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 """
19 """
22 JSON RPC utils
20 JSON RPC utils
23 """
21 """
24
22
25 import collections
23 import collections
26 import logging
24 import logging
27
25
28 from rhodecode.api.exc import JSONRPCError
26 from rhodecode.api.exc import JSONRPCError
29 from rhodecode.lib.auth import (
27 from rhodecode.lib.auth import (
30 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
28 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
31 from rhodecode.lib.str_utils import safe_str
29 from rhodecode.lib.str_utils import safe_str
32 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
33 from rhodecode.lib.view_utils import get_commit_from_ref_name
31 from rhodecode.lib.view_utils import get_commit_from_ref_name
34 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
35
33
36 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
37
35
38
36
39 class OAttr(object):
37 class OAttr(object):
40 """
38 """
41 Special Option that defines other attribute, and can default to them
39 Special Option that defines other attribute, and can default to them
42
40
43 Example::
41 Example::
44
42
45 def test(apiuser, userid=Optional(OAttr('apiuser')):
43 def test(apiuser, userid=Optional(OAttr('apiuser')):
46 user = Optional.extract(userid, evaluate_locals=local())
44 user = Optional.extract(userid, evaluate_locals=local())
47 #if we pass in userid, we get it, else it will default to apiuser
45 #if we pass in userid, we get it, else it will default to apiuser
48 #attribute
46 #attribute
49 """
47 """
50
48
51 def __init__(self, attr_name):
49 def __init__(self, attr_name):
52 self.attr_name = attr_name
50 self.attr_name = attr_name
53
51
54 def __repr__(self):
52 def __repr__(self):
55 return '<OptionalAttr:%s>' % self.attr_name
53 return '<OptionalAttr:%s>' % self.attr_name
56
54
57 def __call__(self):
55 def __call__(self):
58 return self
56 return self
59
57
60
58
61 class Optional(object):
59 class Optional(object):
62 """
60 """
63 Defines an optional parameter::
61 Defines an optional parameter::
64
62
65 param = param.getval() if isinstance(param, Optional) else param
63 param = param.getval() if isinstance(param, Optional) else param
66 param = param() if isinstance(param, Optional) else param
64 param = param() if isinstance(param, Optional) else param
67
65
68 is equivalent of::
66 is equivalent of::
69
67
70 param = Optional.extract(param)
68 param = Optional.extract(param)
71
69
72 """
70 """
73
71
74 def __init__(self, type_):
72 def __init__(self, type_):
75 self.type_ = type_
73 self.type_ = type_
76
74
77 def __repr__(self):
75 def __repr__(self):
78 return '<Optional:%s>' % self.type_.__repr__()
76 return '<Optional:%s>' % self.type_.__repr__()
79
77
80 def __call__(self):
78 def __call__(self):
81 return self.getval()
79 return self.getval()
82
80
83 def getval(self, evaluate_locals=None):
81 def getval(self, evaluate_locals=None):
84 """
82 """
85 returns value from this Optional instance
83 returns value from this Optional instance
86 """
84 """
87 if isinstance(self.type_, OAttr):
85 if isinstance(self.type_, OAttr):
88 param_name = self.type_.attr_name
86 param_name = self.type_.attr_name
89 if evaluate_locals:
87 if evaluate_locals:
90 return evaluate_locals[param_name]
88 return evaluate_locals[param_name]
91 # use params name
89 # use params name
92 return param_name
90 return param_name
93 return self.type_
91 return self.type_
94
92
95 @classmethod
93 @classmethod
96 def extract(cls, val, evaluate_locals=None, binary=None):
94 def extract(cls, val, evaluate_locals=None, binary=None):
97 """
95 """
98 Extracts value from Optional() instance
96 Extracts value from Optional() instance
99
97
100 :param val:
98 :param val:
101 :return: original value if it's not Optional instance else
99 :return: original value if it's not Optional instance else
102 value of instance
100 value of instance
103 """
101 """
104 if isinstance(val, cls):
102 if isinstance(val, cls):
105 val = val.getval(evaluate_locals)
103 val = val.getval(evaluate_locals)
106
104
107 if binary:
105 if binary:
108 val = str2bool(val)
106 val = str2bool(val)
109
107
110 return val
108 return val
111
109
112
110
113 def parse_args(cli_args, key_prefix=''):
111 def parse_args(cli_args, key_prefix=''):
114 from rhodecode.lib.utils2 import (escape_split)
112 from rhodecode.lib.utils2 import (escape_split)
115 kwargs = collections.defaultdict(dict)
113 kwargs = collections.defaultdict(dict)
116 for el in escape_split(cli_args, ','):
114 for el in escape_split(cli_args, ','):
117 kv = escape_split(el, '=', 1)
115 kv = escape_split(el, '=', 1)
118 if len(kv) == 2:
116 if len(kv) == 2:
119 k, v = kv
117 k, v = kv
120 kwargs[key_prefix + k] = v
118 kwargs[key_prefix + k] = v
121 return kwargs
119 return kwargs
122
120
123
121
124 def get_origin(obj):
122 def get_origin(obj):
125 """
123 """
126 Get origin of permission from object.
124 Get origin of permission from object.
127
125
128 :param obj:
126 :param obj:
129 """
127 """
130 origin = 'permission'
128 origin = 'permission'
131
129
132 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
130 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
133 # admin and owner case, maybe we should use dual string ?
131 # admin and owner case, maybe we should use dual string ?
134 origin = 'owner'
132 origin = 'owner'
135 elif getattr(obj, 'owner_row', ''):
133 elif getattr(obj, 'owner_row', ''):
136 origin = 'owner'
134 origin = 'owner'
137 elif getattr(obj, 'admin_row', ''):
135 elif getattr(obj, 'admin_row', ''):
138 origin = 'super-admin'
136 origin = 'super-admin'
139 return origin
137 return origin
140
138
141
139
142 def store_update(updates, attr, name):
140 def store_update(updates, attr, name):
143 """
141 """
144 Stores param in updates dict if it's not instance of Optional
142 Stores param in updates dict if it's not instance of Optional
145 allows easy updates of passed in params
143 allows easy updates of passed in params
146 """
144 """
147 if not isinstance(attr, Optional):
145 if not isinstance(attr, Optional):
148 updates[name] = attr
146 updates[name] = attr
149
147
150
148
151 def has_superadmin_permission(apiuser):
149 def has_superadmin_permission(apiuser):
152 """
150 """
153 Return True if apiuser is admin or return False
151 Return True if apiuser is admin or return False
154
152
155 :param apiuser:
153 :param apiuser:
156 """
154 """
157 if HasPermissionAnyApi('hg.admin')(user=apiuser):
155 if HasPermissionAnyApi('hg.admin')(user=apiuser):
158 return True
156 return True
159 return False
157 return False
160
158
161
159
162 def validate_repo_permissions(apiuser, repoid, repo, perms):
160 def validate_repo_permissions(apiuser, repoid, repo, perms):
163 """
161 """
164 Raise JsonRPCError if apiuser is not authorized or return True
162 Raise JsonRPCError if apiuser is not authorized or return True
165
163
166 :param apiuser:
164 :param apiuser:
167 :param repoid:
165 :param repoid:
168 :param repo:
166 :param repo:
169 :param perms:
167 :param perms:
170 """
168 """
171 if not HasRepoPermissionAnyApi(*perms)(
169 if not HasRepoPermissionAnyApi(*perms)(
172 user=apiuser, repo_name=repo.repo_name):
170 user=apiuser, repo_name=repo.repo_name):
173 raise JSONRPCError('repository `%s` does not exist' % repoid)
171 raise JSONRPCError('repository `%s` does not exist' % repoid)
174
172
175 return True
173 return True
176
174
177
175
178 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
176 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
179 """
177 """
180 Raise JsonRPCError if apiuser is not authorized or return True
178 Raise JsonRPCError if apiuser is not authorized or return True
181
179
182 :param apiuser:
180 :param apiuser:
183 :param repogroupid: just the id of repository group
181 :param repogroupid: just the id of repository group
184 :param repo_group: instance of repo_group
182 :param repo_group: instance of repo_group
185 :param perms:
183 :param perms:
186 """
184 """
187 if not HasRepoGroupPermissionAnyApi(*perms)(
185 if not HasRepoGroupPermissionAnyApi(*perms)(
188 user=apiuser, group_name=repo_group.group_name):
186 user=apiuser, group_name=repo_group.group_name):
189 raise JSONRPCError(
187 raise JSONRPCError(
190 'repository group `%s` does not exist' % repogroupid)
188 'repository group `%s` does not exist' % repogroupid)
191
189
192 return True
190 return True
193
191
194
192
195 def validate_set_owner_permissions(apiuser, owner):
193 def validate_set_owner_permissions(apiuser, owner):
196 if isinstance(owner, Optional):
194 if isinstance(owner, Optional):
197 owner = get_user_or_error(apiuser.user_id)
195 owner = get_user_or_error(apiuser.user_id)
198 else:
196 else:
199 if has_superadmin_permission(apiuser):
197 if has_superadmin_permission(apiuser):
200 owner = get_user_or_error(owner)
198 owner = get_user_or_error(owner)
201 else:
199 else:
202 # forbid setting owner for non-admins
200 # forbid setting owner for non-admins
203 raise JSONRPCError(
201 raise JSONRPCError(
204 'Only RhodeCode super-admin can specify `owner` param')
202 'Only RhodeCode super-admin can specify `owner` param')
205 return owner
203 return owner
206
204
207
205
208 def get_user_or_error(userid):
206 def get_user_or_error(userid):
209 """
207 """
210 Get user by id or name or return JsonRPCError if not found
208 Get user by id or name or return JsonRPCError if not found
211
209
212 :param userid:
210 :param userid:
213 """
211 """
214 from rhodecode.model.user import UserModel
212 from rhodecode.model.user import UserModel
215 user_model = UserModel()
213 user_model = UserModel()
216
214
217 if isinstance(userid, int):
215 if isinstance(userid, int):
218 try:
216 try:
219 user = user_model.get_user(userid)
217 user = user_model.get_user(userid)
220 except ValueError:
218 except ValueError:
221 user = None
219 user = None
222 else:
220 else:
223 user = user_model.get_by_username(userid)
221 user = user_model.get_by_username(userid)
224
222
225 if user is None:
223 if user is None:
226 raise JSONRPCError(
224 raise JSONRPCError(
227 'user `%s` does not exist' % (userid,))
225 'user `{}` does not exist'.format(userid))
228 return user
226 return user
229
227
230
228
231 def get_repo_or_error(repoid):
229 def get_repo_or_error(repoid):
232 """
230 """
233 Get repo by id or name or return JsonRPCError if not found
231 Get repo by id or name or return JsonRPCError if not found
234
232
235 :param repoid:
233 :param repoid:
236 """
234 """
237 from rhodecode.model.repo import RepoModel
235 from rhodecode.model.repo import RepoModel
238 repo_model = RepoModel()
236 repo_model = RepoModel()
239
237
240 if isinstance(repoid, int):
238 if isinstance(repoid, int):
241 try:
239 try:
242 repo = repo_model.get_repo(repoid)
240 repo = repo_model.get_repo(repoid)
243 except ValueError:
241 except ValueError:
244 repo = None
242 repo = None
245 else:
243 else:
246 repo = repo_model.get_by_repo_name(repoid)
244 repo = repo_model.get_by_repo_name(repoid)
247
245
248 if repo is None:
246 if repo is None:
249 raise JSONRPCError(
247 raise JSONRPCError(
250 'repository `%s` does not exist' % (repoid,))
248 'repository `{}` does not exist'.format(repoid))
251 return repo
249 return repo
252
250
253
251
254 def get_repo_group_or_error(repogroupid):
252 def get_repo_group_or_error(repogroupid):
255 """
253 """
256 Get repo group by id or name or return JsonRPCError if not found
254 Get repo group by id or name or return JsonRPCError if not found
257
255
258 :param repogroupid:
256 :param repogroupid:
259 """
257 """
260 from rhodecode.model.repo_group import RepoGroupModel
258 from rhodecode.model.repo_group import RepoGroupModel
261 repo_group_model = RepoGroupModel()
259 repo_group_model = RepoGroupModel()
262
260
263 if isinstance(repogroupid, int):
261 if isinstance(repogroupid, int):
264 try:
262 try:
265 repo_group = repo_group_model._get_repo_group(repogroupid)
263 repo_group = repo_group_model._get_repo_group(repogroupid)
266 except ValueError:
264 except ValueError:
267 repo_group = None
265 repo_group = None
268 else:
266 else:
269 repo_group = repo_group_model.get_by_group_name(repogroupid)
267 repo_group = repo_group_model.get_by_group_name(repogroupid)
270
268
271 if repo_group is None:
269 if repo_group is None:
272 raise JSONRPCError(
270 raise JSONRPCError(
273 'repository group `%s` does not exist' % (repogroupid,))
271 'repository group `{}` does not exist'.format(repogroupid))
274 return repo_group
272 return repo_group
275
273
276
274
277 def get_user_group_or_error(usergroupid):
275 def get_user_group_or_error(usergroupid):
278 """
276 """
279 Get user group by id or name or return JsonRPCError if not found
277 Get user group by id or name or return JsonRPCError if not found
280
278
281 :param usergroupid:
279 :param usergroupid:
282 """
280 """
283 from rhodecode.model.user_group import UserGroupModel
281 from rhodecode.model.user_group import UserGroupModel
284 user_group_model = UserGroupModel()
282 user_group_model = UserGroupModel()
285
283
286 if isinstance(usergroupid, int):
284 if isinstance(usergroupid, int):
287 try:
285 try:
288 user_group = user_group_model.get_group(usergroupid)
286 user_group = user_group_model.get_group(usergroupid)
289 except ValueError:
287 except ValueError:
290 user_group = None
288 user_group = None
291 else:
289 else:
292 user_group = user_group_model.get_by_name(usergroupid)
290 user_group = user_group_model.get_by_name(usergroupid)
293
291
294 if user_group is None:
292 if user_group is None:
295 raise JSONRPCError(
293 raise JSONRPCError(
296 'user group `%s` does not exist' % (usergroupid,))
294 'user group `{}` does not exist'.format(usergroupid))
297 return user_group
295 return user_group
298
296
299
297
300 def get_perm_or_error(permid, prefix=None):
298 def get_perm_or_error(permid, prefix=None):
301 """
299 """
302 Get permission by id or name or return JsonRPCError if not found
300 Get permission by id or name or return JsonRPCError if not found
303
301
304 :param permid:
302 :param permid:
305 """
303 """
306 from rhodecode.model.permission import PermissionModel
304 from rhodecode.model.permission import PermissionModel
307
305
308 perm = PermissionModel.cls.get_by_key(permid)
306 perm = PermissionModel.cls.get_by_key(permid)
309 if perm is None:
307 if perm is None:
310 msg = 'permission `{}` does not exist.'.format(permid)
308 msg = f'permission `{permid}` does not exist.'
311 if prefix:
309 if prefix:
312 msg += ' Permission should start with prefix: `{}`'.format(prefix)
310 msg += f' Permission should start with prefix: `{prefix}`'
313 raise JSONRPCError(msg)
311 raise JSONRPCError(msg)
314
312
315 if prefix:
313 if prefix:
316 if not perm.permission_name.startswith(prefix):
314 if not perm.permission_name.startswith(prefix):
317 raise JSONRPCError('permission `%s` is invalid, '
315 raise JSONRPCError('permission `%s` is invalid, '
318 'should start with %s' % (permid, prefix))
316 'should start with %s' % (permid, prefix))
319 return perm
317 return perm
320
318
321
319
322 def get_gist_or_error(gistid):
320 def get_gist_or_error(gistid):
323 """
321 """
324 Get gist by id or gist_access_id or return JsonRPCError if not found
322 Get gist by id or gist_access_id or return JsonRPCError if not found
325
323
326 :param gistid:
324 :param gistid:
327 """
325 """
328 from rhodecode.model.gist import GistModel
326 from rhodecode.model.gist import GistModel
329
327
330 gist = GistModel.cls.get_by_access_id(gistid)
328 gist = GistModel.cls.get_by_access_id(gistid)
331 if gist is None:
329 if gist is None:
332 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
330 raise JSONRPCError('gist `{}` does not exist'.format(gistid))
333 return gist
331 return gist
334
332
335
333
336 def get_pull_request_or_error(pullrequestid):
334 def get_pull_request_or_error(pullrequestid):
337 """
335 """
338 Get pull request by id or return JsonRPCError if not found
336 Get pull request by id or return JsonRPCError if not found
339
337
340 :param pullrequestid:
338 :param pullrequestid:
341 """
339 """
342 from rhodecode.model.pull_request import PullRequestModel
340 from rhodecode.model.pull_request import PullRequestModel
343
341
344 try:
342 try:
345 pull_request = PullRequestModel().get(int(pullrequestid))
343 pull_request = PullRequestModel().get(int(pullrequestid))
346 except ValueError:
344 except ValueError:
347 raise JSONRPCError('pullrequestid must be an integer')
345 raise JSONRPCError('pullrequestid must be an integer')
348 if not pull_request:
346 if not pull_request:
349 raise JSONRPCError('pull request `%s` does not exist' % (
347 raise JSONRPCError('pull request `{}` does not exist'.format(
350 pullrequestid,))
348 pullrequestid))
351 return pull_request
349 return pull_request
352
350
353
351
354 def build_commit_data(rhodecode_vcs_repo, commit, detail_level):
352 def build_commit_data(rhodecode_vcs_repo, commit, detail_level):
355 commit2 = commit
353 commit2 = commit
356 commit1 = commit.first_parent
354 commit1 = commit.first_parent
357
355
358 parsed_diff = []
356 parsed_diff = []
359 if detail_level == 'extended':
357 if detail_level == 'extended':
360 for f_path in commit.added_paths:
358 for f_path in commit.added_paths:
361 parsed_diff.append(_get_commit_dict(filename=f_path, op='A'))
359 parsed_diff.append(_get_commit_dict(filename=f_path, op='A'))
362 for f_path in commit.changed_paths:
360 for f_path in commit.changed_paths:
363 parsed_diff.append(_get_commit_dict(filename=f_path, op='M'))
361 parsed_diff.append(_get_commit_dict(filename=f_path, op='M'))
364 for f_path in commit.removed_paths:
362 for f_path in commit.removed_paths:
365 parsed_diff.append(_get_commit_dict(filename=f_path, op='D'))
363 parsed_diff.append(_get_commit_dict(filename=f_path, op='D'))
366
364
367 elif detail_level == 'full':
365 elif detail_level == 'full':
368 from rhodecode.lib import diffs
366 from rhodecode.lib import diffs
369
367
370 _diff = rhodecode_vcs_repo.get_diff(commit1, commit2,)
368 _diff = rhodecode_vcs_repo.get_diff(commit1, commit2,)
371 diff_processor = diffs.DiffProcessor(_diff, diff_format='newdiff', show_full_diff=True)
369 diff_processor = diffs.DiffProcessor(_diff, diff_format='newdiff', show_full_diff=True)
372
370
373 for dp in diff_processor.prepare():
371 for dp in diff_processor.prepare():
374 del dp['stats']['ops']
372 del dp['stats']['ops']
375 _stats = dp['stats']
373 _stats = dp['stats']
376 parsed_diff.append(_get_commit_dict(
374 parsed_diff.append(_get_commit_dict(
377 filename=dp['filename'], op=dp['operation'],
375 filename=dp['filename'], op=dp['operation'],
378 new_revision=dp['new_revision'],
376 new_revision=dp['new_revision'],
379 old_revision=dp['old_revision'],
377 old_revision=dp['old_revision'],
380 raw_diff=dp['raw_diff'], stats=_stats))
378 raw_diff=dp['raw_diff'], stats=_stats))
381
379
382 return parsed_diff
380 return parsed_diff
383
381
384
382
385 def get_commit_or_error(ref, repo):
383 def get_commit_or_error(ref, repo):
386 try:
384 try:
387 ref_type, _, ref_hash = ref.split(':')
385 ref_type, _, ref_hash = ref.split(':')
388 except ValueError:
386 except ValueError:
389 raise JSONRPCError(
387 raise JSONRPCError(
390 'Ref `{ref}` given in a wrong format. Please check the API'
388 'Ref `{ref}` given in a wrong format. Please check the API'
391 ' documentation for more details'.format(ref=ref))
389 ' documentation for more details'.format(ref=ref))
392 try:
390 try:
393 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
391 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
394 # once get_commit supports ref_types
392 # once get_commit supports ref_types
395 return get_commit_from_ref_name(repo, ref_hash)
393 return get_commit_from_ref_name(repo, ref_hash)
396 except RepositoryError:
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 def _get_ref_hash(repo, type_, name):
398 def _get_ref_hash(repo, type_, name):
401 vcs_repo = repo.scm_instance()
399 vcs_repo = repo.scm_instance()
402 if type_ in ['branch'] and vcs_repo.alias in ('hg', 'git'):
400 if type_ in ['branch'] and vcs_repo.alias in ('hg', 'git'):
403 return vcs_repo.branches[name]
401 return vcs_repo.branches[name]
404 elif type_ in ['bookmark', 'book'] and vcs_repo.alias == 'hg':
402 elif type_ in ['bookmark', 'book'] and vcs_repo.alias == 'hg':
405 return vcs_repo.bookmarks[name]
403 return vcs_repo.bookmarks[name]
406 else:
404 else:
407 raise ValueError()
405 raise ValueError()
408
406
409
407
410 def resolve_ref_or_error(ref, repo, allowed_ref_types=None):
408 def resolve_ref_or_error(ref, repo, allowed_ref_types=None):
411 allowed_ref_types = allowed_ref_types or ['bookmark', 'book', 'tag', 'branch']
409 allowed_ref_types = allowed_ref_types or ['bookmark', 'book', 'tag', 'branch']
412
410
413 def _parse_ref(type_, name, hash_=None):
411 def _parse_ref(type_, name, hash_=None):
414 return type_, name, hash_
412 return type_, name, hash_
415
413
416 try:
414 try:
417 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
415 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
418 except TypeError:
416 except TypeError:
419 raise JSONRPCError(
417 raise JSONRPCError(
420 'Ref `{ref}` given in a wrong format. Please check the API'
418 'Ref `{ref}` given in a wrong format. Please check the API'
421 ' documentation for more details'.format(ref=ref))
419 ' documentation for more details'.format(ref=ref))
422
420
423 if ref_type not in allowed_ref_types:
421 if ref_type not in allowed_ref_types:
424 raise JSONRPCError(
422 raise JSONRPCError(
425 'Ref `{ref}` type is not allowed. '
423 'Ref `{ref}` type is not allowed. '
426 'Only:{allowed_refs} are possible.'.format(
424 'Only:{allowed_refs} are possible.'.format(
427 ref=ref, allowed_refs=allowed_ref_types))
425 ref=ref, allowed_refs=allowed_ref_types))
428
426
429 try:
427 try:
430 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
428 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
431 except (KeyError, ValueError):
429 except (KeyError, ValueError):
432 raise JSONRPCError(
430 raise JSONRPCError(
433 'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
431 'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
434 type=ref_type, name=ref_name))
432 type=ref_type, name=ref_name))
435
433
436 return ':'.join([ref_type, ref_name, ref_hash])
434 return ':'.join([ref_type, ref_name, ref_hash])
437
435
438
436
439 def _get_commit_dict(
437 def _get_commit_dict(
440 filename, op, new_revision=None, old_revision=None,
438 filename, op, new_revision=None, old_revision=None,
441 raw_diff=None, stats=None):
439 raw_diff=None, stats=None):
442 if stats is None:
440 if stats is None:
443 stats = {
441 stats = {
444 "added": None,
442 "added": None,
445 "binary": None,
443 "binary": None,
446 "deleted": None
444 "deleted": None
447 }
445 }
448 return {
446 return {
449 "filename": safe_str(filename),
447 "filename": safe_str(filename),
450 "op": op,
448 "op": op,
451
449
452 # extra details
450 # extra details
453 "new_revision": new_revision,
451 "new_revision": new_revision,
454 "old_revision": old_revision,
452 "old_revision": old_revision,
455
453
456 "raw_diff": raw_diff,
454 "raw_diff": raw_diff,
457 "stats": stats
455 "stats": stats
458 }
456 }
@@ -1,19 +1,17 b''
1
2
3 # Copyright (C) 2015-2023 RhodeCode GmbH
1 # Copyright (C) 2015-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,102 +1,100 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 """
20 """
23 NOTE:
21 NOTE:
24 Place for deprecated APIs here, if a call needs to be deprecated, please
22 Place for deprecated APIs here, if a call needs to be deprecated, please
25 put it here, and point to a new version
23 put it here, and point to a new version
26 """
24 """
27 import logging
25 import logging
28
26
29 from rhodecode.api import jsonrpc_method, jsonrpc_deprecated_method
27 from rhodecode.api import jsonrpc_method, jsonrpc_deprecated_method
30 from rhodecode.api.utils import Optional, OAttr
28 from rhodecode.api.utils import Optional, OAttr
31
29
32
30
33 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
34
32
35
33
36 # permission check inside
34 # permission check inside
37 @jsonrpc_method()
35 @jsonrpc_method()
38 @jsonrpc_deprecated_method(
36 @jsonrpc_deprecated_method(
39 use_method='comment_commit', deprecated_at_version='3.4.0')
37 use_method='comment_commit', deprecated_at_version='3.4.0')
40 def changeset_comment(request, apiuser, repoid, revision, message,
38 def changeset_comment(request, apiuser, repoid, revision, message,
41 userid=Optional(OAttr('apiuser')),
39 userid=Optional(OAttr('apiuser')),
42 status=Optional(None)):
40 status=Optional(None)):
43 """
41 """
44 Set a changeset comment, and optionally change the status of the
42 Set a changeset comment, and optionally change the status of the
45 changeset.
43 changeset.
46
44
47 This command can only be run using an |authtoken| with admin
45 This command can only be run using an |authtoken| with admin
48 permissions on the |repo|.
46 permissions on the |repo|.
49
47
50 :param apiuser: This is filled automatically from the |authtoken|.
48 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
49 :type apiuser: AuthUser
52 :param repoid: Set the repository name or repository ID.
50 :param repoid: Set the repository name or repository ID.
53 :type repoid: str or int
51 :type repoid: str or int
54 :param revision: Specify the revision for which to set a comment.
52 :param revision: Specify the revision for which to set a comment.
55 :type revision: str
53 :type revision: str
56 :param message: The comment text.
54 :param message: The comment text.
57 :type message: str
55 :type message: str
58 :param userid: Set the user name of the comment creator.
56 :param userid: Set the user name of the comment creator.
59 :type userid: Optional(str or int)
57 :type userid: Optional(str or int)
60 :param status: Set the comment status. The following are valid options:
58 :param status: Set the comment status. The following are valid options:
61 * not_reviewed
59 * not_reviewed
62 * approved
60 * approved
63 * rejected
61 * rejected
64 * under_review
62 * under_review
65 :type status: str
63 :type status: str
66
64
67 Example error output:
65 Example error output:
68
66
69 .. code-block:: javascript
67 .. code-block:: javascript
70
68
71 {
69 {
72 "id" : <id_given_in_input>,
70 "id" : <id_given_in_input>,
73 "result" : {
71 "result" : {
74 "msg": "Commented on commit `<revision>` for repository `<repoid>`",
72 "msg": "Commented on commit `<revision>` for repository `<repoid>`",
75 "status_change": null or <status>,
73 "status_change": null or <status>,
76 "success": true
74 "success": true
77 },
75 },
78 "error" : null
76 "error" : null
79 }
77 }
80
78
81 """
79 """
82 from .repo_api import comment_commit
80 from .repo_api import comment_commit
83
81
84 return comment_commit(request=request,
82 return comment_commit(request=request,
85 apiuser=apiuser, repoid=repoid, commit_id=revision,
83 apiuser=apiuser, repoid=repoid, commit_id=revision,
86 message=message, userid=userid, status=status)
84 message=message, userid=userid, status=status)
87
85
88
86
89 @jsonrpc_method()
87 @jsonrpc_method()
90 @jsonrpc_deprecated_method(
88 @jsonrpc_deprecated_method(
91 use_method='get_ip', deprecated_at_version='4.0.0')
89 use_method='get_ip', deprecated_at_version='4.0.0')
92 def show_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
90 def show_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
93 from .server_api import get_ip
91 from .server_api import get_ip
94 return get_ip(request=request, apiuser=apiuser, userid=userid)
92 return get_ip(request=request, apiuser=apiuser, userid=userid)
95
93
96
94
97 @jsonrpc_method()
95 @jsonrpc_method()
98 @jsonrpc_deprecated_method(
96 @jsonrpc_deprecated_method(
99 use_method='get_user_locks', deprecated_at_version='4.0.0')
97 use_method='get_user_locks', deprecated_at_version='4.0.0')
100 def get_locks(request, apiuser, userid=Optional(OAttr('apiuser'))):
98 def get_locks(request, apiuser, userid=Optional(OAttr('apiuser'))):
101 from .user_api import get_user_locks
99 from .user_api import get_user_locks
102 return get_user_locks(request=request, apiuser=apiuser, userid=userid) No newline at end of file
100 return get_user_locks(request=request, apiuser=apiuser, userid=userid)
@@ -1,257 +1,255 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 import logging
20 import logging
23 import time
21 import time
24
22
25 from rhodecode.api import jsonrpc_method, JSONRPCError
23 from rhodecode.api import jsonrpc_method, JSONRPCError
26 from rhodecode.api.exc import JSONRPCValidationError
24 from rhodecode.api.exc import JSONRPCValidationError
27 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
28 Optional, OAttr, get_gist_or_error, get_user_or_error,
26 Optional, OAttr, get_gist_or_error, get_user_or_error,
29 has_superadmin_permission)
27 has_superadmin_permission)
30 from rhodecode.model.db import Session, or_
28 from rhodecode.model.db import Session, or_
31 from rhodecode.model.gist import Gist, GistModel
29 from rhodecode.model.gist import Gist, GistModel
32
30
33 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
34
32
35
33
36 @jsonrpc_method()
34 @jsonrpc_method()
37 def get_gist(request, apiuser, gistid, content=Optional(False)):
35 def get_gist(request, apiuser, gistid, content=Optional(False)):
38 """
36 """
39 Get the specified gist, based on the gist ID.
37 Get the specified gist, based on the gist ID.
40
38
41 :param apiuser: This is filled automatically from the |authtoken|.
39 :param apiuser: This is filled automatically from the |authtoken|.
42 :type apiuser: AuthUser
40 :type apiuser: AuthUser
43 :param gistid: Set the id of the private or public gist
41 :param gistid: Set the id of the private or public gist
44 :type gistid: str
42 :type gistid: str
45 :param content: Return the gist content. Default is false.
43 :param content: Return the gist content. Default is false.
46 :type content: Optional(bool)
44 :type content: Optional(bool)
47 """
45 """
48
46
49 gist = get_gist_or_error(gistid)
47 gist = get_gist_or_error(gistid)
50 content = Optional.extract(content)
48 content = Optional.extract(content)
51
49
52 if not has_superadmin_permission(apiuser):
50 if not has_superadmin_permission(apiuser):
53 if gist.gist_owner != apiuser.user_id:
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 data = gist.get_api_data()
53 data = gist.get_api_data()
56
54
57 if content:
55 if content:
58 from rhodecode.model.gist import GistModel
56 from rhodecode.model.gist import GistModel
59 rev, gist_files = GistModel().get_gist_files(gistid)
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 return data
59 return data
62
60
63
61
64 @jsonrpc_method()
62 @jsonrpc_method()
65 def get_gists(request, apiuser, userid=Optional(OAttr('apiuser'))):
63 def get_gists(request, apiuser, userid=Optional(OAttr('apiuser'))):
66 """
64 """
67 Get all gists for given user. If userid is empty returned gists
65 Get all gists for given user. If userid is empty returned gists
68 are for user who called the api
66 are for user who called the api
69
67
70 :param apiuser: This is filled automatically from the |authtoken|.
68 :param apiuser: This is filled automatically from the |authtoken|.
71 :type apiuser: AuthUser
69 :type apiuser: AuthUser
72 :param userid: user to get gists for
70 :param userid: user to get gists for
73 :type userid: Optional(str or int)
71 :type userid: Optional(str or int)
74 """
72 """
75
73
76 if not has_superadmin_permission(apiuser):
74 if not has_superadmin_permission(apiuser):
77 # make sure normal user does not pass someone else userid,
75 # make sure normal user does not pass someone else userid,
78 # he is not allowed to do that
76 # he is not allowed to do that
79 if not isinstance(userid, Optional) and userid != apiuser.user_id:
77 if not isinstance(userid, Optional) and userid != apiuser.user_id:
80 raise JSONRPCError(
78 raise JSONRPCError(
81 'userid is not the same as your user'
79 'userid is not the same as your user'
82 )
80 )
83
81
84 if isinstance(userid, Optional):
82 if isinstance(userid, Optional):
85 user_id = apiuser.user_id
83 user_id = apiuser.user_id
86 else:
84 else:
87 user_id = get_user_or_error(userid).user_id
85 user_id = get_user_or_error(userid).user_id
88
86
89 gists = []
87 gists = []
90 _gists = Gist().query() \
88 _gists = Gist().query() \
91 .filter(or_(
89 .filter(or_(
92 Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
90 Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
93 .filter(Gist.gist_owner == user_id) \
91 .filter(Gist.gist_owner == user_id) \
94 .order_by(Gist.created_on.desc())
92 .order_by(Gist.created_on.desc())
95 for gist in _gists:
93 for gist in _gists:
96 gists.append(gist.get_api_data())
94 gists.append(gist.get_api_data())
97 return gists
95 return gists
98
96
99
97
100 @jsonrpc_method()
98 @jsonrpc_method()
101 def create_gist(
99 def create_gist(
102 request, apiuser, files, gistid=Optional(None),
100 request, apiuser, files, gistid=Optional(None),
103 owner=Optional(OAttr('apiuser')),
101 owner=Optional(OAttr('apiuser')),
104 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
102 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
105 acl_level=Optional(Gist.ACL_LEVEL_PUBLIC),
103 acl_level=Optional(Gist.ACL_LEVEL_PUBLIC),
106 description=Optional('')):
104 description=Optional('')):
107 """
105 """
108 Creates a new Gist.
106 Creates a new Gist.
109
107
110 :param apiuser: This is filled automatically from the |authtoken|.
108 :param apiuser: This is filled automatically from the |authtoken|.
111 :type apiuser: AuthUser
109 :type apiuser: AuthUser
112 :param files: files to be added to the gist. The data structure has
110 :param files: files to be added to the gist. The data structure has
113 to match the following example::
111 to match the following example::
114
112
115 {'filename1': {'content':'...'}, 'filename2': {'content':'...'}}
113 {'filename1': {'content':'...'}, 'filename2': {'content':'...'}}
116
114
117 :type files: dict
115 :type files: dict
118 :param gistid: Set a custom id for the gist
116 :param gistid: Set a custom id for the gist
119 :type gistid: Optional(str)
117 :type gistid: Optional(str)
120 :param owner: Set the gist owner, defaults to api method caller
118 :param owner: Set the gist owner, defaults to api method caller
121 :type owner: Optional(str or int)
119 :type owner: Optional(str or int)
122 :param gist_type: type of gist ``public`` or ``private``
120 :param gist_type: type of gist ``public`` or ``private``
123 :type gist_type: Optional(str)
121 :type gist_type: Optional(str)
124 :param lifetime: time in minutes of gist lifetime
122 :param lifetime: time in minutes of gist lifetime
125 :type lifetime: Optional(int)
123 :type lifetime: Optional(int)
126 :param acl_level: acl level for this gist, can be
124 :param acl_level: acl level for this gist, can be
127 ``acl_public`` or ``acl_private`` If the value is set to
125 ``acl_public`` or ``acl_private`` If the value is set to
128 ``acl_private`` only logged in users are able to access this gist.
126 ``acl_private`` only logged in users are able to access this gist.
129 If not set it defaults to ``acl_public``.
127 If not set it defaults to ``acl_public``.
130 :type acl_level: Optional(str)
128 :type acl_level: Optional(str)
131 :param description: gist description
129 :param description: gist description
132 :type description: Optional(str)
130 :type description: Optional(str)
133
131
134 Example output:
132 Example output:
135
133
136 .. code-block:: bash
134 .. code-block:: bash
137
135
138 id : <id_given_in_input>
136 id : <id_given_in_input>
139 result : {
137 result : {
140 "msg": "created new gist",
138 "msg": "created new gist",
141 "gist": {}
139 "gist": {}
142 }
140 }
143 error : null
141 error : null
144
142
145 Example error output:
143 Example error output:
146
144
147 .. code-block:: bash
145 .. code-block:: bash
148
146
149 id : <id_given_in_input>
147 id : <id_given_in_input>
150 result : null
148 result : null
151 error : {
149 error : {
152 "failed to create gist"
150 "failed to create gist"
153 }
151 }
154
152
155 """
153 """
156 from rhodecode.model import validation_schema
154 from rhodecode.model import validation_schema
157 from rhodecode.model.validation_schema.schemas import gist_schema
155 from rhodecode.model.validation_schema.schemas import gist_schema
158
156
159 if isinstance(owner, Optional):
157 if isinstance(owner, Optional):
160 owner = apiuser.user_id
158 owner = apiuser.user_id
161
159
162 owner = get_user_or_error(owner)
160 owner = get_user_or_error(owner)
163
161
164 lifetime = Optional.extract(lifetime)
162 lifetime = Optional.extract(lifetime)
165 schema = gist_schema.GistSchema().bind(
163 schema = gist_schema.GistSchema().bind(
166 # bind the given values if it's allowed, however the deferred
164 # bind the given values if it's allowed, however the deferred
167 # validator will still validate it according to other rules
165 # validator will still validate it according to other rules
168 lifetime_options=[lifetime])
166 lifetime_options=[lifetime])
169
167
170 try:
168 try:
171 nodes = gist_schema.nodes_to_sequence(
169 nodes = gist_schema.nodes_to_sequence(
172 files, colander_node=schema.get('nodes'))
170 files, colander_node=schema.get('nodes'))
173
171
174 schema_data = schema.deserialize(dict(
172 schema_data = schema.deserialize(dict(
175 gistid=Optional.extract(gistid),
173 gistid=Optional.extract(gistid),
176 description=Optional.extract(description),
174 description=Optional.extract(description),
177 gist_type=Optional.extract(gist_type),
175 gist_type=Optional.extract(gist_type),
178 lifetime=lifetime,
176 lifetime=lifetime,
179 gist_acl_level=Optional.extract(acl_level),
177 gist_acl_level=Optional.extract(acl_level),
180 nodes=nodes
178 nodes=nodes
181 ))
179 ))
182
180
183 # convert to safer format with just KEYs so we sure no duplicates
181 # convert to safer format with just KEYs so we sure no duplicates
184 schema_data['nodes'] = gist_schema.sequence_to_nodes(
182 schema_data['nodes'] = gist_schema.sequence_to_nodes(
185 schema_data['nodes'], colander_node=schema.get('nodes'))
183 schema_data['nodes'], colander_node=schema.get('nodes'))
186
184
187 except validation_schema.Invalid as err:
185 except validation_schema.Invalid as err:
188 raise JSONRPCValidationError(colander_exc=err)
186 raise JSONRPCValidationError(colander_exc=err)
189
187
190 try:
188 try:
191 gist = GistModel().create(
189 gist = GistModel().create(
192 owner=owner,
190 owner=owner,
193 gist_id=schema_data['gistid'],
191 gist_id=schema_data['gistid'],
194 description=schema_data['description'],
192 description=schema_data['description'],
195 gist_mapping=schema_data['nodes'],
193 gist_mapping=schema_data['nodes'],
196 gist_type=schema_data['gist_type'],
194 gist_type=schema_data['gist_type'],
197 lifetime=schema_data['lifetime'],
195 lifetime=schema_data['lifetime'],
198 gist_acl_level=schema_data['gist_acl_level'])
196 gist_acl_level=schema_data['gist_acl_level'])
199 Session().commit()
197 Session().commit()
200 return {
198 return {
201 'msg': 'created new gist',
199 'msg': 'created new gist',
202 'gist': gist.get_api_data()
200 'gist': gist.get_api_data()
203 }
201 }
204 except Exception:
202 except Exception:
205 log.exception('Error occurred during creation of gist')
203 log.exception('Error occurred during creation of gist')
206 raise JSONRPCError('failed to create gist')
204 raise JSONRPCError('failed to create gist')
207
205
208
206
209 @jsonrpc_method()
207 @jsonrpc_method()
210 def delete_gist(request, apiuser, gistid):
208 def delete_gist(request, apiuser, gistid):
211 """
209 """
212 Deletes existing gist
210 Deletes existing gist
213
211
214 :param apiuser: filled automatically from apikey
212 :param apiuser: filled automatically from apikey
215 :type apiuser: AuthUser
213 :type apiuser: AuthUser
216 :param gistid: id of gist to delete
214 :param gistid: id of gist to delete
217 :type gistid: str
215 :type gistid: str
218
216
219 Example output:
217 Example output:
220
218
221 .. code-block:: bash
219 .. code-block:: bash
222
220
223 id : <id_given_in_input>
221 id : <id_given_in_input>
224 result : {
222 result : {
225 "deleted gist ID: <gist_id>",
223 "deleted gist ID: <gist_id>",
226 "gist": null
224 "gist": null
227 }
225 }
228 error : null
226 error : null
229
227
230 Example error output:
228 Example error output:
231
229
232 .. code-block:: bash
230 .. code-block:: bash
233
231
234 id : <id_given_in_input>
232 id : <id_given_in_input>
235 result : null
233 result : null
236 error : {
234 error : {
237 "failed to delete gist ID:<gist_id>"
235 "failed to delete gist ID:<gist_id>"
238 }
236 }
239
237
240 """
238 """
241
239
242 gist = get_gist_or_error(gistid)
240 gist = get_gist_or_error(gistid)
243 if not has_superadmin_permission(apiuser):
241 if not has_superadmin_permission(apiuser):
244 if gist.gist_owner != apiuser.user_id:
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 try:
245 try:
248 GistModel().delete(gist)
246 GistModel().delete(gist)
249 Session().commit()
247 Session().commit()
250 return {
248 return {
251 'msg': 'deleted gist ID:%s' % (gist.gist_access_id,),
249 'msg': 'deleted gist ID:{}'.format(gist.gist_access_id),
252 'gist': None
250 'gist': None
253 }
251 }
254 except Exception:
252 except Exception:
255 log.exception('Error occured during gist deletion')
253 log.exception('Error occured during gist deletion')
256 raise JSONRPCError('failed to delete gist ID:%s'
254 raise JSONRPCError('failed to delete gist ID:%s'
257 % (gist.gist_access_id,)) No newline at end of file
255 % (gist.gist_access_id,))
@@ -1,1113 +1,1111 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 import logging
20 import logging
23
21
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
22 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
23 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
24 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
25 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
26 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 from rhodecode.lib import channelstream
27 from rhodecode.lib import channelstream
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
28 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
29 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
30 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.lib.vcs.backends.base import unicode_to_reference
31 from rhodecode.lib.vcs.backends.base import unicode_to_reference
34 from rhodecode.model.changeset_status import ChangesetStatusModel
32 from rhodecode.model.changeset_status import ChangesetStatusModel
35 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
36 from rhodecode.model.db import (
34 from rhodecode.model.db import (
37 Session, ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers)
35 Session, ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers)
38 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
39 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
40 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
41 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
42
40
43 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
44
42
45
43
46 @jsonrpc_method()
44 @jsonrpc_method()
47 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
45 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
48 merge_state=Optional(False)):
46 merge_state=Optional(False)):
49 """
47 """
50 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
51
49
52 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
53 :type apiuser: AuthUser
51 :type apiuser: AuthUser
54 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
55 the pull request was opened.
53 the pull request was opened.
56 :type repoid: str or int
54 :type repoid: str or int
57 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
58 :type pullrequestid: int
56 :type pullrequestid: int
59 :param merge_state: Optional calculate merge state for each repository.
57 :param merge_state: Optional calculate merge state for each repository.
60 This could result in longer time to fetch the data
58 This could result in longer time to fetch the data
61 :type merge_state: bool
59 :type merge_state: bool
62
60
63 Example output:
61 Example output:
64
62
65 .. code-block:: bash
63 .. code-block:: bash
66
64
67 "id": <id_given_in_input>,
65 "id": <id_given_in_input>,
68 "result":
66 "result":
69 {
67 {
70 "pull_request_id": "<pull_request_id>",
68 "pull_request_id": "<pull_request_id>",
71 "url": "<url>",
69 "url": "<url>",
72 "title": "<title>",
70 "title": "<title>",
73 "description": "<description>",
71 "description": "<description>",
74 "status" : "<status>",
72 "status" : "<status>",
75 "created_on": "<date_time_created>",
73 "created_on": "<date_time_created>",
76 "updated_on": "<date_time_updated>",
74 "updated_on": "<date_time_updated>",
77 "versions": "<number_or_versions_of_pr>",
75 "versions": "<number_or_versions_of_pr>",
78 "commit_ids": [
76 "commit_ids": [
79 ...
77 ...
80 "<commit_id>",
78 "<commit_id>",
81 "<commit_id>",
79 "<commit_id>",
82 ...
80 ...
83 ],
81 ],
84 "review_status": "<review_status>",
82 "review_status": "<review_status>",
85 "mergeable": {
83 "mergeable": {
86 "status": "<bool>",
84 "status": "<bool>",
87 "message": "<message>",
85 "message": "<message>",
88 },
86 },
89 "source": {
87 "source": {
90 "clone_url": "<clone_url>",
88 "clone_url": "<clone_url>",
91 "repository": "<repository_name>",
89 "repository": "<repository_name>",
92 "reference":
90 "reference":
93 {
91 {
94 "name": "<name>",
92 "name": "<name>",
95 "type": "<type>",
93 "type": "<type>",
96 "commit_id": "<commit_id>",
94 "commit_id": "<commit_id>",
97 }
95 }
98 },
96 },
99 "target": {
97 "target": {
100 "clone_url": "<clone_url>",
98 "clone_url": "<clone_url>",
101 "repository": "<repository_name>",
99 "repository": "<repository_name>",
102 "reference":
100 "reference":
103 {
101 {
104 "name": "<name>",
102 "name": "<name>",
105 "type": "<type>",
103 "type": "<type>",
106 "commit_id": "<commit_id>",
104 "commit_id": "<commit_id>",
107 }
105 }
108 },
106 },
109 "merge": {
107 "merge": {
110 "clone_url": "<clone_url>",
108 "clone_url": "<clone_url>",
111 "reference":
109 "reference":
112 {
110 {
113 "name": "<name>",
111 "name": "<name>",
114 "type": "<type>",
112 "type": "<type>",
115 "commit_id": "<commit_id>",
113 "commit_id": "<commit_id>",
116 }
114 }
117 },
115 },
118 "author": <user_obj>,
116 "author": <user_obj>,
119 "reviewers": [
117 "reviewers": [
120 ...
118 ...
121 {
119 {
122 "user": "<user_obj>",
120 "user": "<user_obj>",
123 "review_status": "<review_status>",
121 "review_status": "<review_status>",
124 }
122 }
125 ...
123 ...
126 ]
124 ]
127 },
125 },
128 "error": null
126 "error": null
129 """
127 """
130
128
131 pull_request = get_pull_request_or_error(pullrequestid)
129 pull_request = get_pull_request_or_error(pullrequestid)
132 if Optional.extract(repoid):
130 if Optional.extract(repoid):
133 repo = get_repo_or_error(repoid)
131 repo = get_repo_or_error(repoid)
134 else:
132 else:
135 repo = pull_request.target_repo
133 repo = pull_request.target_repo
136
134
137 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
138 raise JSONRPCError('repository `%s` or pull request `%s` '
136 raise JSONRPCError('repository `%s` or pull request `%s` '
139 'does not exist' % (repoid, pullrequestid))
137 'does not exist' % (repoid, pullrequestid))
140
138
141 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
142 # otherwise we can lock the repo on calculation of merge state while update/merge
140 # otherwise we can lock the repo on calculation of merge state while update/merge
143 # is happening.
141 # is happening.
144 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
145 merge_state = Optional.extract(merge_state, binary=True) and pr_created
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
146 data = pull_request.get_api_data(with_merge_state=merge_state)
144 data = pull_request.get_api_data(with_merge_state=merge_state)
147 return data
145 return data
148
146
149
147
150 @jsonrpc_method()
148 @jsonrpc_method()
151 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
152 merge_state=Optional(False)):
150 merge_state=Optional(False)):
153 """
151 """
154 Get all pull requests from the repository specified in `repoid`.
152 Get all pull requests from the repository specified in `repoid`.
155
153
156 :param apiuser: This is filled automatically from the |authtoken|.
154 :param apiuser: This is filled automatically from the |authtoken|.
157 :type apiuser: AuthUser
155 :type apiuser: AuthUser
158 :param repoid: Optional repository name or repository ID.
156 :param repoid: Optional repository name or repository ID.
159 :type repoid: str or int
157 :type repoid: str or int
160 :param status: Only return pull requests with the specified status.
158 :param status: Only return pull requests with the specified status.
161 Valid options are.
159 Valid options are.
162 * ``new`` (default)
160 * ``new`` (default)
163 * ``open``
161 * ``open``
164 * ``closed``
162 * ``closed``
165 :type status: str
163 :type status: str
166 :param merge_state: Optional calculate merge state for each repository.
164 :param merge_state: Optional calculate merge state for each repository.
167 This could result in longer time to fetch the data
165 This could result in longer time to fetch the data
168 :type merge_state: bool
166 :type merge_state: bool
169
167
170 Example output:
168 Example output:
171
169
172 .. code-block:: bash
170 .. code-block:: bash
173
171
174 "id": <id_given_in_input>,
172 "id": <id_given_in_input>,
175 "result":
173 "result":
176 [
174 [
177 ...
175 ...
178 {
176 {
179 "pull_request_id": "<pull_request_id>",
177 "pull_request_id": "<pull_request_id>",
180 "url": "<url>",
178 "url": "<url>",
181 "title" : "<title>",
179 "title" : "<title>",
182 "description": "<description>",
180 "description": "<description>",
183 "status": "<status>",
181 "status": "<status>",
184 "created_on": "<date_time_created>",
182 "created_on": "<date_time_created>",
185 "updated_on": "<date_time_updated>",
183 "updated_on": "<date_time_updated>",
186 "commit_ids": [
184 "commit_ids": [
187 ...
185 ...
188 "<commit_id>",
186 "<commit_id>",
189 "<commit_id>",
187 "<commit_id>",
190 ...
188 ...
191 ],
189 ],
192 "review_status": "<review_status>",
190 "review_status": "<review_status>",
193 "mergeable": {
191 "mergeable": {
194 "status": "<bool>",
192 "status": "<bool>",
195 "message: "<message>",
193 "message: "<message>",
196 },
194 },
197 "source": {
195 "source": {
198 "clone_url": "<clone_url>",
196 "clone_url": "<clone_url>",
199 "reference":
197 "reference":
200 {
198 {
201 "name": "<name>",
199 "name": "<name>",
202 "type": "<type>",
200 "type": "<type>",
203 "commit_id": "<commit_id>",
201 "commit_id": "<commit_id>",
204 }
202 }
205 },
203 },
206 "target": {
204 "target": {
207 "clone_url": "<clone_url>",
205 "clone_url": "<clone_url>",
208 "reference":
206 "reference":
209 {
207 {
210 "name": "<name>",
208 "name": "<name>",
211 "type": "<type>",
209 "type": "<type>",
212 "commit_id": "<commit_id>",
210 "commit_id": "<commit_id>",
213 }
211 }
214 },
212 },
215 "merge": {
213 "merge": {
216 "clone_url": "<clone_url>",
214 "clone_url": "<clone_url>",
217 "reference":
215 "reference":
218 {
216 {
219 "name": "<name>",
217 "name": "<name>",
220 "type": "<type>",
218 "type": "<type>",
221 "commit_id": "<commit_id>",
219 "commit_id": "<commit_id>",
222 }
220 }
223 },
221 },
224 "author": <user_obj>,
222 "author": <user_obj>,
225 "reviewers": [
223 "reviewers": [
226 ...
224 ...
227 {
225 {
228 "user": "<user_obj>",
226 "user": "<user_obj>",
229 "review_status": "<review_status>",
227 "review_status": "<review_status>",
230 }
228 }
231 ...
229 ...
232 ]
230 ]
233 }
231 }
234 ...
232 ...
235 ],
233 ],
236 "error": null
234 "error": null
237
235
238 """
236 """
239 repo = get_repo_or_error(repoid)
237 repo = get_repo_or_error(repoid)
240 if not has_superadmin_permission(apiuser):
238 if not has_superadmin_permission(apiuser):
241 _perms = (
239 _perms = (
242 'repository.admin', 'repository.write', 'repository.read',)
240 'repository.admin', 'repository.write', 'repository.read',)
243 validate_repo_permissions(apiuser, repoid, repo, _perms)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
244
242
245 status = Optional.extract(status)
243 status = Optional.extract(status)
246 merge_state = Optional.extract(merge_state, binary=True)
244 merge_state = Optional.extract(merge_state, binary=True)
247 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
248 order_by='id', order_dir='desc')
246 order_by='id', order_dir='desc')
249 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
250 return data
248 return data
251
249
252
250
253 @jsonrpc_method()
251 @jsonrpc_method()
254 def merge_pull_request(
252 def merge_pull_request(
255 request, apiuser, pullrequestid, repoid=Optional(None),
253 request, apiuser, pullrequestid, repoid=Optional(None),
256 userid=Optional(OAttr('apiuser'))):
254 userid=Optional(OAttr('apiuser'))):
257 """
255 """
258 Merge the pull request specified by `pullrequestid` into its target
256 Merge the pull request specified by `pullrequestid` into its target
259 repository.
257 repository.
260
258
261 :param apiuser: This is filled automatically from the |authtoken|.
259 :param apiuser: This is filled automatically from the |authtoken|.
262 :type apiuser: AuthUser
260 :type apiuser: AuthUser
263 :param repoid: Optional, repository name or repository ID of the
261 :param repoid: Optional, repository name or repository ID of the
264 target repository to which the |pr| is to be merged.
262 target repository to which the |pr| is to be merged.
265 :type repoid: str or int
263 :type repoid: str or int
266 :param pullrequestid: ID of the pull request which shall be merged.
264 :param pullrequestid: ID of the pull request which shall be merged.
267 :type pullrequestid: int
265 :type pullrequestid: int
268 :param userid: Merge the pull request as this user.
266 :param userid: Merge the pull request as this user.
269 :type userid: Optional(str or int)
267 :type userid: Optional(str or int)
270
268
271 Example output:
269 Example output:
272
270
273 .. code-block:: bash
271 .. code-block:: bash
274
272
275 "id": <id_given_in_input>,
273 "id": <id_given_in_input>,
276 "result": {
274 "result": {
277 "executed": "<bool>",
275 "executed": "<bool>",
278 "failure_reason": "<int>",
276 "failure_reason": "<int>",
279 "merge_status_message": "<str>",
277 "merge_status_message": "<str>",
280 "merge_commit_id": "<merge_commit_id>",
278 "merge_commit_id": "<merge_commit_id>",
281 "possible": "<bool>",
279 "possible": "<bool>",
282 "merge_ref": {
280 "merge_ref": {
283 "commit_id": "<commit_id>",
281 "commit_id": "<commit_id>",
284 "type": "<type>",
282 "type": "<type>",
285 "name": "<name>"
283 "name": "<name>"
286 }
284 }
287 },
285 },
288 "error": null
286 "error": null
289 """
287 """
290 pull_request = get_pull_request_or_error(pullrequestid)
288 pull_request = get_pull_request_or_error(pullrequestid)
291 if Optional.extract(repoid):
289 if Optional.extract(repoid):
292 repo = get_repo_or_error(repoid)
290 repo = get_repo_or_error(repoid)
293 else:
291 else:
294 repo = pull_request.target_repo
292 repo = pull_request.target_repo
295 auth_user = apiuser
293 auth_user = apiuser
296
294
297 if not isinstance(userid, Optional):
295 if not isinstance(userid, Optional):
298 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
296 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
299 user=apiuser, repo_name=repo.repo_name)
297 user=apiuser, repo_name=repo.repo_name)
300 if has_superadmin_permission(apiuser) or is_repo_admin:
298 if has_superadmin_permission(apiuser) or is_repo_admin:
301 apiuser = get_user_or_error(userid)
299 apiuser = get_user_or_error(userid)
302 auth_user = apiuser.AuthUser()
300 auth_user = apiuser.AuthUser()
303 else:
301 else:
304 raise JSONRPCError('userid is not the same as your user')
302 raise JSONRPCError('userid is not the same as your user')
305
303
306 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
307 raise JSONRPCError(
305 raise JSONRPCError(
308 'Operation forbidden because pull request is in state {}, '
306 'Operation forbidden because pull request is in state {}, '
309 'only state {} is allowed.'.format(
307 'only state {} is allowed.'.format(
310 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
311
309
312 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 with pull_request.set_state(PullRequest.STATE_UPDATING):
313 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
314 translator=request.translate)
312 translator=request.translate)
315 merge_possible = not check.failed
313 merge_possible = not check.failed
316
314
317 if not merge_possible:
315 if not merge_possible:
318 error_messages = []
316 error_messages = []
319 for err_type, error_msg in check.errors:
317 for err_type, error_msg in check.errors:
320 error_msg = request.translate(error_msg)
318 error_msg = request.translate(error_msg)
321 error_messages.append(error_msg)
319 error_messages.append(error_msg)
322
320
323 reasons = ','.join(error_messages)
321 reasons = ','.join(error_messages)
324 raise JSONRPCError(
322 raise JSONRPCError(
325 'merge not possible for following reasons: {}'.format(reasons))
323 f'merge not possible for following reasons: {reasons}')
326
324
327 target_repo = pull_request.target_repo
325 target_repo = pull_request.target_repo
328 extras = vcs_operation_context(
326 extras = vcs_operation_context(
329 request.environ, repo_name=target_repo.repo_name,
327 request.environ, repo_name=target_repo.repo_name,
330 username=auth_user.username, action='push',
328 username=auth_user.username, action='push',
331 scm=target_repo.repo_type)
329 scm=target_repo.repo_type)
332 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 with pull_request.set_state(PullRequest.STATE_UPDATING):
333 merge_response = PullRequestModel().merge_repo(
331 merge_response = PullRequestModel().merge_repo(
334 pull_request, apiuser, extras=extras)
332 pull_request, apiuser, extras=extras)
335 if merge_response.executed:
333 if merge_response.executed:
336 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
337
335
338 Session().commit()
336 Session().commit()
339
337
340 # In previous versions the merge response directly contained the merge
338 # In previous versions the merge response directly contained the merge
341 # commit id. It is now contained in the merge reference object. To be
339 # commit id. It is now contained in the merge reference object. To be
342 # backwards compatible we have to extract it again.
340 # backwards compatible we have to extract it again.
343 merge_response = merge_response.asdict()
341 merge_response = merge_response.asdict()
344 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
345
343
346 return merge_response
344 return merge_response
347
345
348
346
349 @jsonrpc_method()
347 @jsonrpc_method()
350 def get_pull_request_comments(
348 def get_pull_request_comments(
351 request, apiuser, pullrequestid, repoid=Optional(None)):
349 request, apiuser, pullrequestid, repoid=Optional(None)):
352 """
350 """
353 Get all comments of pull request specified with the `pullrequestid`
351 Get all comments of pull request specified with the `pullrequestid`
354
352
355 :param apiuser: This is filled automatically from the |authtoken|.
353 :param apiuser: This is filled automatically from the |authtoken|.
356 :type apiuser: AuthUser
354 :type apiuser: AuthUser
357 :param repoid: Optional repository name or repository ID.
355 :param repoid: Optional repository name or repository ID.
358 :type repoid: str or int
356 :type repoid: str or int
359 :param pullrequestid: The pull request ID.
357 :param pullrequestid: The pull request ID.
360 :type pullrequestid: int
358 :type pullrequestid: int
361
359
362 Example output:
360 Example output:
363
361
364 .. code-block:: bash
362 .. code-block:: bash
365
363
366 id : <id_given_in_input>
364 id : <id_given_in_input>
367 result : [
365 result : [
368 {
366 {
369 "comment_author": {
367 "comment_author": {
370 "active": true,
368 "active": true,
371 "full_name_or_username": "Tom Gore",
369 "full_name_or_username": "Tom Gore",
372 "username": "admin"
370 "username": "admin"
373 },
371 },
374 "comment_created_on": "2017-01-02T18:43:45.533",
372 "comment_created_on": "2017-01-02T18:43:45.533",
375 "comment_f_path": null,
373 "comment_f_path": null,
376 "comment_id": 25,
374 "comment_id": 25,
377 "comment_lineno": null,
375 "comment_lineno": null,
378 "comment_status": {
376 "comment_status": {
379 "status": "under_review",
377 "status": "under_review",
380 "status_lbl": "Under Review"
378 "status_lbl": "Under Review"
381 },
379 },
382 "comment_text": "Example text",
380 "comment_text": "Example text",
383 "comment_type": null,
381 "comment_type": null,
384 "comment_last_version: 0,
382 "comment_last_version: 0,
385 "pull_request_version": null,
383 "pull_request_version": null,
386 "comment_commit_id": None,
384 "comment_commit_id": None,
387 "comment_pull_request_id": <pull_request_id>
385 "comment_pull_request_id": <pull_request_id>
388 }
386 }
389 ],
387 ],
390 error : null
388 error : null
391 """
389 """
392
390
393 pull_request = get_pull_request_or_error(pullrequestid)
391 pull_request = get_pull_request_or_error(pullrequestid)
394 if Optional.extract(repoid):
392 if Optional.extract(repoid):
395 repo = get_repo_or_error(repoid)
393 repo = get_repo_or_error(repoid)
396 else:
394 else:
397 repo = pull_request.target_repo
395 repo = pull_request.target_repo
398
396
399 if not PullRequestModel().check_user_read(
397 if not PullRequestModel().check_user_read(
400 pull_request, apiuser, api=True):
398 pull_request, apiuser, api=True):
401 raise JSONRPCError('repository `%s` or pull request `%s` '
399 raise JSONRPCError('repository `%s` or pull request `%s` '
402 'does not exist' % (repoid, pullrequestid))
400 'does not exist' % (repoid, pullrequestid))
403
401
404 (pull_request_latest,
402 (pull_request_latest,
405 pull_request_at_ver,
403 pull_request_at_ver,
406 pull_request_display_obj,
404 pull_request_display_obj,
407 at_version) = PullRequestModel().get_pr_version(
405 at_version) = PullRequestModel().get_pr_version(
408 pull_request.pull_request_id, version=None)
406 pull_request.pull_request_id, version=None)
409
407
410 versions = pull_request_display_obj.versions()
408 versions = pull_request_display_obj.versions()
411 ver_map = {
409 ver_map = {
412 ver.pull_request_version_id: cnt
410 ver.pull_request_version_id: cnt
413 for cnt, ver in enumerate(versions, 1)
411 for cnt, ver in enumerate(versions, 1)
414 }
412 }
415
413
416 # GENERAL COMMENTS with versions #
414 # GENERAL COMMENTS with versions #
417 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
415 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
418 q = q.order_by(ChangesetComment.comment_id.asc())
416 q = q.order_by(ChangesetComment.comment_id.asc())
419 general_comments = q.all()
417 general_comments = q.all()
420
418
421 # INLINE COMMENTS with versions #
419 # INLINE COMMENTS with versions #
422 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
420 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
423 q = q.order_by(ChangesetComment.comment_id.asc())
421 q = q.order_by(ChangesetComment.comment_id.asc())
424 inline_comments = q.all()
422 inline_comments = q.all()
425
423
426 data = []
424 data = []
427 for comment in inline_comments + general_comments:
425 for comment in inline_comments + general_comments:
428 full_data = comment.get_api_data()
426 full_data = comment.get_api_data()
429 pr_version_id = None
427 pr_version_id = None
430 if comment.pull_request_version_id:
428 if comment.pull_request_version_id:
431 pr_version_id = 'v{}'.format(
429 pr_version_id = 'v{}'.format(
432 ver_map[comment.pull_request_version_id])
430 ver_map[comment.pull_request_version_id])
433
431
434 # sanitize some entries
432 # sanitize some entries
435
433
436 full_data['pull_request_version'] = pr_version_id
434 full_data['pull_request_version'] = pr_version_id
437 full_data['comment_author'] = {
435 full_data['comment_author'] = {
438 'username': full_data['comment_author'].username,
436 'username': full_data['comment_author'].username,
439 'full_name_or_username': full_data['comment_author'].full_name_or_username,
437 'full_name_or_username': full_data['comment_author'].full_name_or_username,
440 'active': full_data['comment_author'].active,
438 'active': full_data['comment_author'].active,
441 }
439 }
442
440
443 if full_data['comment_status']:
441 if full_data['comment_status']:
444 full_data['comment_status'] = {
442 full_data['comment_status'] = {
445 'status': full_data['comment_status'][0].status,
443 'status': full_data['comment_status'][0].status,
446 'status_lbl': full_data['comment_status'][0].status_lbl,
444 'status_lbl': full_data['comment_status'][0].status_lbl,
447 }
445 }
448 else:
446 else:
449 full_data['comment_status'] = {}
447 full_data['comment_status'] = {}
450
448
451 data.append(full_data)
449 data.append(full_data)
452 return data
450 return data
453
451
454
452
455 @jsonrpc_method()
453 @jsonrpc_method()
456 def comment_pull_request(
454 def comment_pull_request(
457 request, apiuser, pullrequestid, repoid=Optional(None),
455 request, apiuser, pullrequestid, repoid=Optional(None),
458 message=Optional(None), commit_id=Optional(None), status=Optional(None),
456 message=Optional(None), commit_id=Optional(None), status=Optional(None),
459 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
457 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
460 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
458 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
461 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
459 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
462 """
460 """
463 Comment on the pull request specified with the `pullrequestid`,
461 Comment on the pull request specified with the `pullrequestid`,
464 in the |repo| specified by the `repoid`, and optionally change the
462 in the |repo| specified by the `repoid`, and optionally change the
465 review status.
463 review status.
466
464
467 :param apiuser: This is filled automatically from the |authtoken|.
465 :param apiuser: This is filled automatically from the |authtoken|.
468 :type apiuser: AuthUser
466 :type apiuser: AuthUser
469 :param repoid: Optional repository name or repository ID.
467 :param repoid: Optional repository name or repository ID.
470 :type repoid: str or int
468 :type repoid: str or int
471 :param pullrequestid: The pull request ID.
469 :param pullrequestid: The pull request ID.
472 :type pullrequestid: int
470 :type pullrequestid: int
473 :param commit_id: Specify the commit_id for which to set a comment. If
471 :param commit_id: Specify the commit_id for which to set a comment. If
474 given commit_id is different than latest in the PR status
472 given commit_id is different than latest in the PR status
475 change won't be performed.
473 change won't be performed.
476 :type commit_id: str
474 :type commit_id: str
477 :param message: The text content of the comment.
475 :param message: The text content of the comment.
478 :type message: str
476 :type message: str
479 :param status: (**Optional**) Set the approval status of the pull
477 :param status: (**Optional**) Set the approval status of the pull
480 request. One of: 'not_reviewed', 'approved', 'rejected',
478 request. One of: 'not_reviewed', 'approved', 'rejected',
481 'under_review'
479 'under_review'
482 :type status: str
480 :type status: str
483 :param comment_type: Comment type, one of: 'note', 'todo'
481 :param comment_type: Comment type, one of: 'note', 'todo'
484 :type comment_type: Optional(str), default: 'note'
482 :type comment_type: Optional(str), default: 'note'
485 :param resolves_comment_id: id of comment which this one will resolve
483 :param resolves_comment_id: id of comment which this one will resolve
486 :type resolves_comment_id: Optional(int)
484 :type resolves_comment_id: Optional(int)
487 :param extra_recipients: list of user ids or usernames to add
485 :param extra_recipients: list of user ids or usernames to add
488 notifications for this comment. Acts like a CC for notification
486 notifications for this comment. Acts like a CC for notification
489 :type extra_recipients: Optional(list)
487 :type extra_recipients: Optional(list)
490 :param userid: Comment on the pull request as this user
488 :param userid: Comment on the pull request as this user
491 :type userid: Optional(str or int)
489 :type userid: Optional(str or int)
492 :param send_email: Define if this comment should also send email notification
490 :param send_email: Define if this comment should also send email notification
493 :type send_email: Optional(bool)
491 :type send_email: Optional(bool)
494
492
495 Example output:
493 Example output:
496
494
497 .. code-block:: bash
495 .. code-block:: bash
498
496
499 id : <id_given_in_input>
497 id : <id_given_in_input>
500 result : {
498 result : {
501 "pull_request_id": "<Integer>",
499 "pull_request_id": "<Integer>",
502 "comment_id": "<Integer>",
500 "comment_id": "<Integer>",
503 "status": {"given": <given_status>,
501 "status": {"given": <given_status>,
504 "was_changed": <bool status_was_actually_changed> },
502 "was_changed": <bool status_was_actually_changed> },
505 },
503 },
506 error : null
504 error : null
507 """
505 """
508 _ = request.translate
506 _ = request.translate
509
507
510 pull_request = get_pull_request_or_error(pullrequestid)
508 pull_request = get_pull_request_or_error(pullrequestid)
511 if Optional.extract(repoid):
509 if Optional.extract(repoid):
512 repo = get_repo_or_error(repoid)
510 repo = get_repo_or_error(repoid)
513 else:
511 else:
514 repo = pull_request.target_repo
512 repo = pull_request.target_repo
515
513
516 db_repo_name = repo.repo_name
514 db_repo_name = repo.repo_name
517 auth_user = apiuser
515 auth_user = apiuser
518 if not isinstance(userid, Optional):
516 if not isinstance(userid, Optional):
519 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
517 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
520 user=apiuser, repo_name=db_repo_name)
518 user=apiuser, repo_name=db_repo_name)
521 if has_superadmin_permission(apiuser) or is_repo_admin:
519 if has_superadmin_permission(apiuser) or is_repo_admin:
522 apiuser = get_user_or_error(userid)
520 apiuser = get_user_or_error(userid)
523 auth_user = apiuser.AuthUser()
521 auth_user = apiuser.AuthUser()
524 else:
522 else:
525 raise JSONRPCError('userid is not the same as your user')
523 raise JSONRPCError('userid is not the same as your user')
526
524
527 if pull_request.is_closed():
525 if pull_request.is_closed():
528 raise JSONRPCError(f'pull request `{pullrequestid}` comment failed, pull request is closed')
526 raise JSONRPCError(f'pull request `{pullrequestid}` comment failed, pull request is closed')
529
527
530 if not PullRequestModel().check_user_read(
528 if not PullRequestModel().check_user_read(
531 pull_request, apiuser, api=True):
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 message = Optional.extract(message)
531 message = Optional.extract(message)
534 status = Optional.extract(status)
532 status = Optional.extract(status)
535 commit_id = Optional.extract(commit_id)
533 commit_id = Optional.extract(commit_id)
536 comment_type = Optional.extract(comment_type)
534 comment_type = Optional.extract(comment_type)
537 resolves_comment_id = Optional.extract(resolves_comment_id)
535 resolves_comment_id = Optional.extract(resolves_comment_id)
538 extra_recipients = Optional.extract(extra_recipients)
536 extra_recipients = Optional.extract(extra_recipients)
539 send_email = Optional.extract(send_email, binary=True)
537 send_email = Optional.extract(send_email, binary=True)
540
538
541 if not message and not status:
539 if not message and not status:
542 raise JSONRPCError(
540 raise JSONRPCError(
543 'Both message and status parameters are missing. '
541 'Both message and status parameters are missing. '
544 'At least one is required.')
542 'At least one is required.')
545
543
546 if status and status not in (st[0] for st in ChangesetStatus.STATUSES):
544 if status and status not in (st[0] for st in ChangesetStatus.STATUSES):
547 raise JSONRPCError(f'Unknown comment status: `{status}`')
545 raise JSONRPCError(f'Unknown comment status: `{status}`')
548
546
549 if commit_id and commit_id not in pull_request.revisions:
547 if commit_id and commit_id not in pull_request.revisions:
550 raise JSONRPCError(f'Invalid commit_id `{commit_id}` for this pull request.')
548 raise JSONRPCError(f'Invalid commit_id `{commit_id}` for this pull request.')
551
549
552 allowed_to_change_status = PullRequestModel().check_user_change_status(
550 allowed_to_change_status = PullRequestModel().check_user_change_status(
553 pull_request, apiuser)
551 pull_request, apiuser)
554
552
555 # if commit_id is passed re-validated if user is allowed to change status
553 # if commit_id is passed re-validated if user is allowed to change status
556 # based on the latest commit_id from the PR
554 # based on the latest commit_id from the PR
557 if commit_id:
555 if commit_id:
558 commit_idx = pull_request.revisions.index(commit_id)
556 commit_idx = pull_request.revisions.index(commit_id)
559 if commit_idx != 0:
557 if commit_idx != 0:
560 log.warning('Resetting allowed_to_change_status = False because commit is NOT the latest in pull-request')
558 log.warning('Resetting allowed_to_change_status = False because commit is NOT the latest in pull-request')
561 allowed_to_change_status = False
559 allowed_to_change_status = False
562
560
563 if resolves_comment_id:
561 if resolves_comment_id:
564 comment = ChangesetComment.get(resolves_comment_id)
562 comment = ChangesetComment.get(resolves_comment_id)
565 if not comment:
563 if not comment:
566 raise JSONRPCError(f'Invalid resolves_comment_id `{resolves_comment_id}` for this pull request.')
564 raise JSONRPCError(f'Invalid resolves_comment_id `{resolves_comment_id}` for this pull request.')
567 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
565 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
568 raise JSONRPCError(f'Comment `{resolves_comment_id}` is wrong type for setting status to resolved.')
566 raise JSONRPCError(f'Comment `{resolves_comment_id}` is wrong type for setting status to resolved.')
569
567
570 text = message
568 text = message
571 status_label = ChangesetStatus.get_status_lbl(status)
569 status_label = ChangesetStatus.get_status_lbl(status)
572 if status and allowed_to_change_status:
570 if status and allowed_to_change_status:
573 st_message = ('Status change %(transition_icon)s %(status)s'
571 st_message = ('Status change %(transition_icon)s %(status)s'
574 % {'transition_icon': '>', 'status': status_label})
572 % {'transition_icon': '>', 'status': status_label})
575 text = message or st_message
573 text = message or st_message
576
574
577 rc_config = SettingsModel().get_all_settings()
575 rc_config = SettingsModel().get_all_settings()
578 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
576 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
579
577
580 status_change = status and allowed_to_change_status
578 status_change = status and allowed_to_change_status
581 comment = CommentsModel().create(
579 comment = CommentsModel().create(
582 text=text,
580 text=text,
583 repo=pull_request.target_repo.repo_id,
581 repo=pull_request.target_repo.repo_id,
584 user=apiuser.user_id,
582 user=apiuser.user_id,
585 pull_request=pull_request.pull_request_id,
583 pull_request=pull_request.pull_request_id,
586 f_path=None,
584 f_path=None,
587 line_no=None,
585 line_no=None,
588 status_change=(status_label if status_change else None),
586 status_change=(status_label if status_change else None),
589 status_change_type=(status if status_change else None),
587 status_change_type=(status if status_change else None),
590 closing_pr=False,
588 closing_pr=False,
591 renderer=renderer,
589 renderer=renderer,
592 comment_type=comment_type,
590 comment_type=comment_type,
593 resolves_comment_id=resolves_comment_id,
591 resolves_comment_id=resolves_comment_id,
594 auth_user=auth_user,
592 auth_user=auth_user,
595 extra_recipients=extra_recipients,
593 extra_recipients=extra_recipients,
596 send_email=send_email
594 send_email=send_email
597 )
595 )
598
596
599 is_inline = comment.is_inline
597 is_inline = comment.is_inline
600
598
601 if allowed_to_change_status and status:
599 if allowed_to_change_status and status:
602 old_calculated_status = pull_request.calculated_review_status()
600 old_calculated_status = pull_request.calculated_review_status()
603 ChangesetStatusModel().set_status(
601 ChangesetStatusModel().set_status(
604 pull_request.target_repo.repo_id,
602 pull_request.target_repo.repo_id,
605 status,
603 status,
606 apiuser.user_id,
604 apiuser.user_id,
607 comment,
605 comment,
608 pull_request=pull_request.pull_request_id
606 pull_request=pull_request.pull_request_id
609 )
607 )
610 Session().flush()
608 Session().flush()
611
609
612 Session().commit()
610 Session().commit()
613
611
614 PullRequestModel().trigger_pull_request_hook(
612 PullRequestModel().trigger_pull_request_hook(
615 pull_request, apiuser, 'comment',
613 pull_request, apiuser, 'comment',
616 data={'comment': comment})
614 data={'comment': comment})
617
615
618 if allowed_to_change_status and status:
616 if allowed_to_change_status and status:
619 # we now calculate the status of pull request, and based on that
617 # we now calculate the status of pull request, and based on that
620 # calculation we set the commits status
618 # calculation we set the commits status
621 calculated_status = pull_request.calculated_review_status()
619 calculated_status = pull_request.calculated_review_status()
622 if old_calculated_status != calculated_status:
620 if old_calculated_status != calculated_status:
623 PullRequestModel().trigger_pull_request_hook(
621 PullRequestModel().trigger_pull_request_hook(
624 pull_request, apiuser, 'review_status_change',
622 pull_request, apiuser, 'review_status_change',
625 data={'status': calculated_status})
623 data={'status': calculated_status})
626
624
627 data = {
625 data = {
628 'pull_request_id': pull_request.pull_request_id,
626 'pull_request_id': pull_request.pull_request_id,
629 'comment_id': comment.comment_id if comment else None,
627 'comment_id': comment.comment_id if comment else None,
630 'status': {'given': status, 'was_changed': status_change},
628 'status': {'given': status, 'was_changed': status_change},
631 }
629 }
632
630
633 comment_broadcast_channel = channelstream.comment_channel(
631 comment_broadcast_channel = channelstream.comment_channel(
634 db_repo_name, pull_request_obj=pull_request)
632 db_repo_name, pull_request_obj=pull_request)
635
633
636 comment_data = data
634 comment_data = data
637 comment_type = 'inline' if is_inline else 'general'
635 comment_type = 'inline' if is_inline else 'general'
638 channelstream.comment_channelstream_push(
636 channelstream.comment_channelstream_push(
639 request, comment_broadcast_channel, apiuser,
637 request, comment_broadcast_channel, apiuser,
640 _('posted a new {} comment').format(comment_type),
638 _('posted a new {} comment').format(comment_type),
641 comment_data=comment_data)
639 comment_data=comment_data)
642
640
643 return data
641 return data
644
642
645
643
646 def _reviewers_validation(obj_list):
644 def _reviewers_validation(obj_list):
647 schema = ReviewerListSchema()
645 schema = ReviewerListSchema()
648 try:
646 try:
649 reviewer_objects = schema.deserialize(obj_list)
647 reviewer_objects = schema.deserialize(obj_list)
650 except Invalid as err:
648 except Invalid as err:
651 raise JSONRPCValidationError(colander_exc=err)
649 raise JSONRPCValidationError(colander_exc=err)
652
650
653 # validate users
651 # validate users
654 for reviewer_object in reviewer_objects:
652 for reviewer_object in reviewer_objects:
655 user = get_user_or_error(reviewer_object['username'])
653 user = get_user_or_error(reviewer_object['username'])
656 reviewer_object['user_id'] = user.user_id
654 reviewer_object['user_id'] = user.user_id
657 return reviewer_objects
655 return reviewer_objects
658
656
659
657
660 @jsonrpc_method()
658 @jsonrpc_method()
661 def create_pull_request(
659 def create_pull_request(
662 request, apiuser, source_repo, target_repo, source_ref, target_ref,
660 request, apiuser, source_repo, target_repo, source_ref, target_ref,
663 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
661 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
664 description_renderer=Optional(''),
662 description_renderer=Optional(''),
665 reviewers=Optional(None), observers=Optional(None)):
663 reviewers=Optional(None), observers=Optional(None)):
666 """
664 """
667 Creates a new pull request.
665 Creates a new pull request.
668
666
669 Accepts refs in the following formats:
667 Accepts refs in the following formats:
670
668
671 * branch:<branch_name>:<sha>
669 * branch:<branch_name>:<sha>
672 * branch:<branch_name>
670 * branch:<branch_name>
673 * bookmark:<bookmark_name>:<sha> (Mercurial only)
671 * bookmark:<bookmark_name>:<sha> (Mercurial only)
674 * bookmark:<bookmark_name> (Mercurial only)
672 * bookmark:<bookmark_name> (Mercurial only)
675
673
676 :param apiuser: This is filled automatically from the |authtoken|.
674 :param apiuser: This is filled automatically from the |authtoken|.
677 :type apiuser: AuthUser
675 :type apiuser: AuthUser
678 :param source_repo: Set the source repository name.
676 :param source_repo: Set the source repository name.
679 :type source_repo: str
677 :type source_repo: str
680 :param target_repo: Set the target repository name.
678 :param target_repo: Set the target repository name.
681 :type target_repo: str
679 :type target_repo: str
682 :param source_ref: Set the source ref name.
680 :param source_ref: Set the source ref name.
683 :type source_ref: str
681 :type source_ref: str
684 :param target_ref: Set the target ref name.
682 :param target_ref: Set the target ref name.
685 :type target_ref: str
683 :type target_ref: str
686 :param owner: user_id or username
684 :param owner: user_id or username
687 :type owner: Optional(str)
685 :type owner: Optional(str)
688 :param title: Optionally Set the pull request title, it's generated otherwise
686 :param title: Optionally Set the pull request title, it's generated otherwise
689 :type title: str
687 :type title: str
690 :param description: Set the pull request description.
688 :param description: Set the pull request description.
691 :type description: Optional(str)
689 :type description: Optional(str)
692 :type description_renderer: Optional(str)
690 :type description_renderer: Optional(str)
693 :param description_renderer: Set pull request renderer for the description.
691 :param description_renderer: Set pull request renderer for the description.
694 It should be 'rst', 'markdown' or 'plain'. If not give default
692 It should be 'rst', 'markdown' or 'plain'. If not give default
695 system renderer will be used
693 system renderer will be used
696 :param reviewers: Set the new pull request reviewers list.
694 :param reviewers: Set the new pull request reviewers list.
697 Reviewer defined by review rules will be added automatically to the
695 Reviewer defined by review rules will be added automatically to the
698 defined list.
696 defined list.
699 :type reviewers: Optional(list)
697 :type reviewers: Optional(list)
700 Accepts username strings or objects of the format:
698 Accepts username strings or objects of the format:
701
699
702 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
700 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
703 :param observers: Set the new pull request observers list.
701 :param observers: Set the new pull request observers list.
704 Reviewer defined by review rules will be added automatically to the
702 Reviewer defined by review rules will be added automatically to the
705 defined list. This feature is only available in RhodeCode EE
703 defined list. This feature is only available in RhodeCode EE
706 :type observers: Optional(list)
704 :type observers: Optional(list)
707 Accepts username strings or objects of the format:
705 Accepts username strings or objects of the format:
708
706
709 [{'username': 'nick', 'reasons': ['original author']}]
707 [{'username': 'nick', 'reasons': ['original author']}]
710 """
708 """
711
709
712 source_db_repo = get_repo_or_error(source_repo)
710 source_db_repo = get_repo_or_error(source_repo)
713 target_db_repo = get_repo_or_error(target_repo)
711 target_db_repo = get_repo_or_error(target_repo)
714 if not has_superadmin_permission(apiuser):
712 if not has_superadmin_permission(apiuser):
715 _perms = ('repository.admin', 'repository.write', 'repository.read',)
713 _perms = ('repository.admin', 'repository.write', 'repository.read',)
716 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
714 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
717
715
718 owner = validate_set_owner_permissions(apiuser, owner)
716 owner = validate_set_owner_permissions(apiuser, owner)
719
717
720 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
718 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
721 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
719 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
722
720
723 get_commit_or_error(full_source_ref, source_db_repo)
721 get_commit_or_error(full_source_ref, source_db_repo)
724 get_commit_or_error(full_target_ref, target_db_repo)
722 get_commit_or_error(full_target_ref, target_db_repo)
725
723
726 reviewer_objects = Optional.extract(reviewers) or []
724 reviewer_objects = Optional.extract(reviewers) or []
727 observer_objects = Optional.extract(observers) or []
725 observer_objects = Optional.extract(observers) or []
728
726
729 # serialize and validate passed in given reviewers
727 # serialize and validate passed in given reviewers
730 if reviewer_objects:
728 if reviewer_objects:
731 reviewer_objects = _reviewers_validation(reviewer_objects)
729 reviewer_objects = _reviewers_validation(reviewer_objects)
732
730
733 if observer_objects:
731 if observer_objects:
734 observer_objects = _reviewers_validation(reviewer_objects)
732 observer_objects = _reviewers_validation(reviewer_objects)
735
733
736 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
734 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
737 PullRequestModel().get_reviewer_functions()
735 PullRequestModel().get_reviewer_functions()
738
736
739 source_ref_obj = unicode_to_reference(full_source_ref)
737 source_ref_obj = unicode_to_reference(full_source_ref)
740 target_ref_obj = unicode_to_reference(full_target_ref)
738 target_ref_obj = unicode_to_reference(full_target_ref)
741
739
742 # recalculate reviewers logic, to make sure we can validate this
740 # recalculate reviewers logic, to make sure we can validate this
743 default_reviewers_data = get_default_reviewers_data(
741 default_reviewers_data = get_default_reviewers_data(
744 owner,
742 owner,
745 source_db_repo,
743 source_db_repo,
746 source_ref_obj,
744 source_ref_obj,
747 target_db_repo,
745 target_db_repo,
748 target_ref_obj,
746 target_ref_obj,
749 )
747 )
750
748
751 # now MERGE our given with the calculated from the default rules
749 # now MERGE our given with the calculated from the default rules
752 just_reviewers = [
750 just_reviewers = [
753 x for x in default_reviewers_data['reviewers']
751 x for x in default_reviewers_data['reviewers']
754 if x['role'] == PullRequestReviewers.ROLE_REVIEWER]
752 if x['role'] == PullRequestReviewers.ROLE_REVIEWER]
755 reviewer_objects = just_reviewers + reviewer_objects
753 reviewer_objects = just_reviewers + reviewer_objects
756
754
757 try:
755 try:
758 reviewers = validate_default_reviewers(
756 reviewers = validate_default_reviewers(
759 reviewer_objects, default_reviewers_data)
757 reviewer_objects, default_reviewers_data)
760 except ValueError as e:
758 except ValueError as e:
761 raise JSONRPCError('Reviewers Validation: {}'.format(e))
759 raise JSONRPCError(f'Reviewers Validation: {e}')
762
760
763 # now MERGE our given with the calculated from the default rules
761 # now MERGE our given with the calculated from the default rules
764 just_observers = [
762 just_observers = [
765 x for x in default_reviewers_data['reviewers']
763 x for x in default_reviewers_data['reviewers']
766 if x['role'] == PullRequestReviewers.ROLE_OBSERVER]
764 if x['role'] == PullRequestReviewers.ROLE_OBSERVER]
767 observer_objects = just_observers + observer_objects
765 observer_objects = just_observers + observer_objects
768
766
769 try:
767 try:
770 observers = validate_observers(
768 observers = validate_observers(
771 observer_objects, default_reviewers_data)
769 observer_objects, default_reviewers_data)
772 except ValueError as e:
770 except ValueError as e:
773 raise JSONRPCError('Observer Validation: {}'.format(e))
771 raise JSONRPCError(f'Observer Validation: {e}')
774
772
775 title = Optional.extract(title)
773 title = Optional.extract(title)
776 if not title:
774 if not title:
777 title_source_ref = source_ref_obj.name
775 title_source_ref = source_ref_obj.name
778 title = PullRequestModel().generate_pullrequest_title(
776 title = PullRequestModel().generate_pullrequest_title(
779 source=source_repo,
777 source=source_repo,
780 source_ref=title_source_ref,
778 source_ref=title_source_ref,
781 target=target_repo
779 target=target_repo
782 )
780 )
783
781
784 diff_info = default_reviewers_data['diff_info']
782 diff_info = default_reviewers_data['diff_info']
785 common_ancestor_id = diff_info['ancestor']
783 common_ancestor_id = diff_info['ancestor']
786 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
784 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
787 commits = [commit['commit_id'] for commit in reversed(diff_info['commits'])]
785 commits = [commit['commit_id'] for commit in reversed(diff_info['commits'])]
788
786
789 if not common_ancestor_id:
787 if not common_ancestor_id:
790 raise JSONRPCError('no common ancestor found between specified references')
788 raise JSONRPCError('no common ancestor found between specified references')
791
789
792 if not commits:
790 if not commits:
793 raise JSONRPCError('no commits found for merge between specified references')
791 raise JSONRPCError('no commits found for merge between specified references')
794
792
795 # recalculate target ref based on ancestor
793 # recalculate target ref based on ancestor
796 full_target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, common_ancestor_id))
794 full_target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, common_ancestor_id))
797
795
798 # fetch renderer, if set fallback to plain in case of PR
796 # fetch renderer, if set fallback to plain in case of PR
799 rc_config = SettingsModel().get_all_settings()
797 rc_config = SettingsModel().get_all_settings()
800 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
798 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
801 description = Optional.extract(description)
799 description = Optional.extract(description)
802 description_renderer = Optional.extract(description_renderer) or default_system_renderer
800 description_renderer = Optional.extract(description_renderer) or default_system_renderer
803
801
804 pull_request = PullRequestModel().create(
802 pull_request = PullRequestModel().create(
805 created_by=owner.user_id,
803 created_by=owner.user_id,
806 source_repo=source_repo,
804 source_repo=source_repo,
807 source_ref=full_source_ref,
805 source_ref=full_source_ref,
808 target_repo=target_repo,
806 target_repo=target_repo,
809 target_ref=full_target_ref,
807 target_ref=full_target_ref,
810 common_ancestor_id=common_ancestor_id,
808 common_ancestor_id=common_ancestor_id,
811 revisions=commits,
809 revisions=commits,
812 reviewers=reviewers,
810 reviewers=reviewers,
813 observers=observers,
811 observers=observers,
814 title=title,
812 title=title,
815 description=description,
813 description=description,
816 description_renderer=description_renderer,
814 description_renderer=description_renderer,
817 reviewer_data=default_reviewers_data,
815 reviewer_data=default_reviewers_data,
818 auth_user=apiuser
816 auth_user=apiuser
819 )
817 )
820
818
821 Session().commit()
819 Session().commit()
822 data = {
820 data = {
823 'msg': 'Created new pull request `{}`'.format(title),
821 'msg': f'Created new pull request `{title}`',
824 'pull_request_id': pull_request.pull_request_id,
822 'pull_request_id': pull_request.pull_request_id,
825 }
823 }
826 return data
824 return data
827
825
828
826
829 @jsonrpc_method()
827 @jsonrpc_method()
830 def update_pull_request(
828 def update_pull_request(
831 request, apiuser, pullrequestid, repoid=Optional(None),
829 request, apiuser, pullrequestid, repoid=Optional(None),
832 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
830 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
833 reviewers=Optional(None), observers=Optional(None), update_commits=Optional(None)):
831 reviewers=Optional(None), observers=Optional(None), update_commits=Optional(None)):
834 """
832 """
835 Updates a pull request.
833 Updates a pull request.
836
834
837 :param apiuser: This is filled automatically from the |authtoken|.
835 :param apiuser: This is filled automatically from the |authtoken|.
838 :type apiuser: AuthUser
836 :type apiuser: AuthUser
839 :param repoid: Optional repository name or repository ID.
837 :param repoid: Optional repository name or repository ID.
840 :type repoid: str or int
838 :type repoid: str or int
841 :param pullrequestid: The pull request ID.
839 :param pullrequestid: The pull request ID.
842 :type pullrequestid: int
840 :type pullrequestid: int
843 :param title: Set the pull request title.
841 :param title: Set the pull request title.
844 :type title: str
842 :type title: str
845 :param description: Update pull request description.
843 :param description: Update pull request description.
846 :type description: Optional(str)
844 :type description: Optional(str)
847 :type description_renderer: Optional(str)
845 :type description_renderer: Optional(str)
848 :param description_renderer: Update pull request renderer for the description.
846 :param description_renderer: Update pull request renderer for the description.
849 It should be 'rst', 'markdown' or 'plain'
847 It should be 'rst', 'markdown' or 'plain'
850 :param reviewers: Update pull request reviewers list with new value.
848 :param reviewers: Update pull request reviewers list with new value.
851 :type reviewers: Optional(list)
849 :type reviewers: Optional(list)
852 Accepts username strings or objects of the format:
850 Accepts username strings or objects of the format:
853
851
854 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
852 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
855 :param observers: Update pull request observers list with new value.
853 :param observers: Update pull request observers list with new value.
856 :type observers: Optional(list)
854 :type observers: Optional(list)
857 Accepts username strings or objects of the format:
855 Accepts username strings or objects of the format:
858
856
859 [{'username': 'nick', 'reasons': ['should be aware about this PR']}]
857 [{'username': 'nick', 'reasons': ['should be aware about this PR']}]
860 :param update_commits: Trigger update of commits for this pull request
858 :param update_commits: Trigger update of commits for this pull request
861 :type: update_commits: Optional(bool)
859 :type: update_commits: Optional(bool)
862
860
863 Example output:
861 Example output:
864
862
865 .. code-block:: bash
863 .. code-block:: bash
866
864
867 id : <id_given_in_input>
865 id : <id_given_in_input>
868 result : {
866 result : {
869 "msg": "Updated pull request `63`",
867 "msg": "Updated pull request `63`",
870 "pull_request": <pull_request_object>,
868 "pull_request": <pull_request_object>,
871 "updated_reviewers": {
869 "updated_reviewers": {
872 "added": [
870 "added": [
873 "username"
871 "username"
874 ],
872 ],
875 "removed": []
873 "removed": []
876 },
874 },
877 "updated_observers": {
875 "updated_observers": {
878 "added": [
876 "added": [
879 "username"
877 "username"
880 ],
878 ],
881 "removed": []
879 "removed": []
882 },
880 },
883 "updated_commits": {
881 "updated_commits": {
884 "added": [
882 "added": [
885 "<sha1_hash>"
883 "<sha1_hash>"
886 ],
884 ],
887 "common": [
885 "common": [
888 "<sha1_hash>",
886 "<sha1_hash>",
889 "<sha1_hash>",
887 "<sha1_hash>",
890 ],
888 ],
891 "removed": []
889 "removed": []
892 }
890 }
893 }
891 }
894 error : null
892 error : null
895 """
893 """
896
894
897 pull_request = get_pull_request_or_error(pullrequestid)
895 pull_request = get_pull_request_or_error(pullrequestid)
898 if Optional.extract(repoid):
896 if Optional.extract(repoid):
899 repo = get_repo_or_error(repoid)
897 repo = get_repo_or_error(repoid)
900 else:
898 else:
901 repo = pull_request.target_repo
899 repo = pull_request.target_repo
902
900
903 if not PullRequestModel().check_user_update(
901 if not PullRequestModel().check_user_update(
904 pull_request, apiuser, api=True):
902 pull_request, apiuser, api=True):
905 raise JSONRPCError(
903 raise JSONRPCError(
906 'pull request `%s` update failed, no permission to update.' % (
904 'pull request `{}` update failed, no permission to update.'.format(
907 pullrequestid,))
905 pullrequestid))
908 if pull_request.is_closed():
906 if pull_request.is_closed():
909 raise JSONRPCError(
907 raise JSONRPCError(
910 'pull request `%s` update failed, pull request is closed' % (
908 'pull request `{}` update failed, pull request is closed'.format(
911 pullrequestid,))
909 pullrequestid))
912
910
913 reviewer_objects = Optional.extract(reviewers) or []
911 reviewer_objects = Optional.extract(reviewers) or []
914 observer_objects = Optional.extract(observers) or []
912 observer_objects = Optional.extract(observers) or []
915
913
916 title = Optional.extract(title)
914 title = Optional.extract(title)
917 description = Optional.extract(description)
915 description = Optional.extract(description)
918 description_renderer = Optional.extract(description_renderer)
916 description_renderer = Optional.extract(description_renderer)
919
917
920 # Update title/description
918 # Update title/description
921 title_changed = False
919 title_changed = False
922 if title or description:
920 if title or description:
923 PullRequestModel().edit(
921 PullRequestModel().edit(
924 pull_request,
922 pull_request,
925 title or pull_request.title,
923 title or pull_request.title,
926 description or pull_request.description,
924 description or pull_request.description,
927 description_renderer or pull_request.description_renderer,
925 description_renderer or pull_request.description_renderer,
928 apiuser)
926 apiuser)
929 Session().commit()
927 Session().commit()
930 title_changed = True
928 title_changed = True
931
929
932 commit_changes = {"added": [], "common": [], "removed": []}
930 commit_changes = {"added": [], "common": [], "removed": []}
933
931
934 # Update commits
932 # Update commits
935 commits_changed = False
933 commits_changed = False
936 if str2bool(Optional.extract(update_commits)):
934 if str2bool(Optional.extract(update_commits)):
937
935
938 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
936 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
939 raise JSONRPCError(
937 raise JSONRPCError(
940 'Operation forbidden because pull request is in state {}, '
938 'Operation forbidden because pull request is in state {}, '
941 'only state {} is allowed.'.format(
939 'only state {} is allowed.'.format(
942 pull_request.pull_request_state, PullRequest.STATE_CREATED))
940 pull_request.pull_request_state, PullRequest.STATE_CREATED))
943
941
944 with pull_request.set_state(PullRequest.STATE_UPDATING):
942 with pull_request.set_state(PullRequest.STATE_UPDATING):
945 if PullRequestModel().has_valid_update_type(pull_request):
943 if PullRequestModel().has_valid_update_type(pull_request):
946 db_user = apiuser.get_instance()
944 db_user = apiuser.get_instance()
947 update_response = PullRequestModel().update_commits(
945 update_response = PullRequestModel().update_commits(
948 pull_request, db_user)
946 pull_request, db_user)
949 commit_changes = update_response.changes or commit_changes
947 commit_changes = update_response.changes or commit_changes
950 Session().commit()
948 Session().commit()
951 commits_changed = True
949 commits_changed = True
952
950
953 # Update reviewers
951 # Update reviewers
954 # serialize and validate passed in given reviewers
952 # serialize and validate passed in given reviewers
955 if reviewer_objects:
953 if reviewer_objects:
956 reviewer_objects = _reviewers_validation(reviewer_objects)
954 reviewer_objects = _reviewers_validation(reviewer_objects)
957
955
958 if observer_objects:
956 if observer_objects:
959 observer_objects = _reviewers_validation(reviewer_objects)
957 observer_objects = _reviewers_validation(reviewer_objects)
960
958
961 # re-use stored rules
959 # re-use stored rules
962 default_reviewers_data = pull_request.reviewer_data
960 default_reviewers_data = pull_request.reviewer_data
963
961
964 __, validate_default_reviewers, validate_observers = \
962 __, validate_default_reviewers, validate_observers = \
965 PullRequestModel().get_reviewer_functions()
963 PullRequestModel().get_reviewer_functions()
966
964
967 if reviewer_objects:
965 if reviewer_objects:
968 try:
966 try:
969 reviewers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
967 reviewers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
970 except ValueError as e:
968 except ValueError as e:
971 raise JSONRPCError('Reviewers Validation: {}'.format(e))
969 raise JSONRPCError(f'Reviewers Validation: {e}')
972 else:
970 else:
973 reviewers = []
971 reviewers = []
974
972
975 if observer_objects:
973 if observer_objects:
976 try:
974 try:
977 observers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
975 observers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
978 except ValueError as e:
976 except ValueError as e:
979 raise JSONRPCError('Observer Validation: {}'.format(e))
977 raise JSONRPCError(f'Observer Validation: {e}')
980 else:
978 else:
981 observers = []
979 observers = []
982
980
983 reviewers_changed = False
981 reviewers_changed = False
984 reviewers_changes = {"added": [], "removed": []}
982 reviewers_changes = {"added": [], "removed": []}
985 if reviewers:
983 if reviewers:
986 old_calculated_status = pull_request.calculated_review_status()
984 old_calculated_status = pull_request.calculated_review_status()
987 added_reviewers, removed_reviewers = \
985 added_reviewers, removed_reviewers = \
988 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser.get_instance())
986 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser.get_instance())
989
987
990 reviewers_changes['added'] = sorted(
988 reviewers_changes['added'] = sorted(
991 [get_user_or_error(n).username for n in added_reviewers])
989 [get_user_or_error(n).username for n in added_reviewers])
992 reviewers_changes['removed'] = sorted(
990 reviewers_changes['removed'] = sorted(
993 [get_user_or_error(n).username for n in removed_reviewers])
991 [get_user_or_error(n).username for n in removed_reviewers])
994 Session().commit()
992 Session().commit()
995
993
996 # trigger status changed if change in reviewers changes the status
994 # trigger status changed if change in reviewers changes the status
997 calculated_status = pull_request.calculated_review_status()
995 calculated_status = pull_request.calculated_review_status()
998 if old_calculated_status != calculated_status:
996 if old_calculated_status != calculated_status:
999 PullRequestModel().trigger_pull_request_hook(
997 PullRequestModel().trigger_pull_request_hook(
1000 pull_request, apiuser, 'review_status_change',
998 pull_request, apiuser, 'review_status_change',
1001 data={'status': calculated_status})
999 data={'status': calculated_status})
1002 reviewers_changed = True
1000 reviewers_changed = True
1003
1001
1004 observers_changed = False
1002 observers_changed = False
1005 observers_changes = {"added": [], "removed": []}
1003 observers_changes = {"added": [], "removed": []}
1006 if observers:
1004 if observers:
1007 added_observers, removed_observers = \
1005 added_observers, removed_observers = \
1008 PullRequestModel().update_observers(pull_request, observers, apiuser.get_instance())
1006 PullRequestModel().update_observers(pull_request, observers, apiuser.get_instance())
1009
1007
1010 observers_changes['added'] = sorted(
1008 observers_changes['added'] = sorted(
1011 [get_user_or_error(n).username for n in added_observers])
1009 [get_user_or_error(n).username for n in added_observers])
1012 observers_changes['removed'] = sorted(
1010 observers_changes['removed'] = sorted(
1013 [get_user_or_error(n).username for n in removed_observers])
1011 [get_user_or_error(n).username for n in removed_observers])
1014 Session().commit()
1012 Session().commit()
1015
1013
1016 reviewers_changed = True
1014 reviewers_changed = True
1017
1015
1018 # push changed to channelstream
1016 # push changed to channelstream
1019 if commits_changed or reviewers_changed or observers_changed:
1017 if commits_changed or reviewers_changed or observers_changed:
1020 pr_broadcast_channel = channelstream.pr_channel(pull_request)
1018 pr_broadcast_channel = channelstream.pr_channel(pull_request)
1021 msg = 'Pull request was updated.'
1019 msg = 'Pull request was updated.'
1022 channelstream.pr_update_channelstream_push(
1020 channelstream.pr_update_channelstream_push(
1023 request, pr_broadcast_channel, apiuser, msg)
1021 request, pr_broadcast_channel, apiuser, msg)
1024
1022
1025 data = {
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 'pull_request': pull_request.get_api_data(),
1025 'pull_request': pull_request.get_api_data(),
1028 'updated_commits': commit_changes,
1026 'updated_commits': commit_changes,
1029 'updated_reviewers': reviewers_changes,
1027 'updated_reviewers': reviewers_changes,
1030 'updated_observers': observers_changes,
1028 'updated_observers': observers_changes,
1031 }
1029 }
1032
1030
1033 return data
1031 return data
1034
1032
1035
1033
1036 @jsonrpc_method()
1034 @jsonrpc_method()
1037 def close_pull_request(
1035 def close_pull_request(
1038 request, apiuser, pullrequestid, repoid=Optional(None),
1036 request, apiuser, pullrequestid, repoid=Optional(None),
1039 userid=Optional(OAttr('apiuser')), message=Optional('')):
1037 userid=Optional(OAttr('apiuser')), message=Optional('')):
1040 """
1038 """
1041 Close the pull request specified by `pullrequestid`.
1039 Close the pull request specified by `pullrequestid`.
1042
1040
1043 :param apiuser: This is filled automatically from the |authtoken|.
1041 :param apiuser: This is filled automatically from the |authtoken|.
1044 :type apiuser: AuthUser
1042 :type apiuser: AuthUser
1045 :param repoid: Repository name or repository ID to which the pull
1043 :param repoid: Repository name or repository ID to which the pull
1046 request belongs.
1044 request belongs.
1047 :type repoid: str or int
1045 :type repoid: str or int
1048 :param pullrequestid: ID of the pull request to be closed.
1046 :param pullrequestid: ID of the pull request to be closed.
1049 :type pullrequestid: int
1047 :type pullrequestid: int
1050 :param userid: Close the pull request as this user.
1048 :param userid: Close the pull request as this user.
1051 :type userid: Optional(str or int)
1049 :type userid: Optional(str or int)
1052 :param message: Optional message to close the Pull Request with. If not
1050 :param message: Optional message to close the Pull Request with. If not
1053 specified it will be generated automatically.
1051 specified it will be generated automatically.
1054 :type message: Optional(str)
1052 :type message: Optional(str)
1055
1053
1056 Example output:
1054 Example output:
1057
1055
1058 .. code-block:: bash
1056 .. code-block:: bash
1059
1057
1060 "id": <id_given_in_input>,
1058 "id": <id_given_in_input>,
1061 "result": {
1059 "result": {
1062 "pull_request_id": "<int>",
1060 "pull_request_id": "<int>",
1063 "close_status": "<str:status_lbl>,
1061 "close_status": "<str:status_lbl>,
1064 "closed": "<bool>"
1062 "closed": "<bool>"
1065 },
1063 },
1066 "error": null
1064 "error": null
1067
1065
1068 """
1066 """
1069 _ = request.translate
1067 _ = request.translate
1070
1068
1071 pull_request = get_pull_request_or_error(pullrequestid)
1069 pull_request = get_pull_request_or_error(pullrequestid)
1072 if Optional.extract(repoid):
1070 if Optional.extract(repoid):
1073 repo = get_repo_or_error(repoid)
1071 repo = get_repo_or_error(repoid)
1074 else:
1072 else:
1075 repo = pull_request.target_repo
1073 repo = pull_request.target_repo
1076
1074
1077 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1075 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1078 user=apiuser, repo_name=repo.repo_name)
1076 user=apiuser, repo_name=repo.repo_name)
1079 if not isinstance(userid, Optional):
1077 if not isinstance(userid, Optional):
1080 if has_superadmin_permission(apiuser) or is_repo_admin:
1078 if has_superadmin_permission(apiuser) or is_repo_admin:
1081 apiuser = get_user_or_error(userid)
1079 apiuser = get_user_or_error(userid)
1082 else:
1080 else:
1083 raise JSONRPCError('userid is not the same as your user')
1081 raise JSONRPCError('userid is not the same as your user')
1084
1082
1085 if pull_request.is_closed():
1083 if pull_request.is_closed():
1086 raise JSONRPCError(
1084 raise JSONRPCError(
1087 'pull request `%s` is already closed' % (pullrequestid,))
1085 'pull request `{}` is already closed'.format(pullrequestid))
1088
1086
1089 # only owner or admin or person with write permissions
1087 # only owner or admin or person with write permissions
1090 allowed_to_close = PullRequestModel().check_user_update(
1088 allowed_to_close = PullRequestModel().check_user_update(
1091 pull_request, apiuser, api=True)
1089 pull_request, apiuser, api=True)
1092
1090
1093 if not allowed_to_close:
1091 if not allowed_to_close:
1094 raise JSONRPCError(
1092 raise JSONRPCError(
1095 'pull request `%s` close failed, no permission to close.' % (
1093 'pull request `{}` close failed, no permission to close.'.format(
1096 pullrequestid,))
1094 pullrequestid))
1097
1095
1098 # message we're using to close the PR, else it's automatically generated
1096 # message we're using to close the PR, else it's automatically generated
1099 message = Optional.extract(message)
1097 message = Optional.extract(message)
1100
1098
1101 # finally close the PR, with proper message comment
1099 # finally close the PR, with proper message comment
1102 comment, status = PullRequestModel().close_pull_request_with_comment(
1100 comment, status = PullRequestModel().close_pull_request_with_comment(
1103 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1101 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1104 status_lbl = ChangesetStatus.get_status_lbl(status)
1102 status_lbl = ChangesetStatus.get_status_lbl(status)
1105
1103
1106 Session().commit()
1104 Session().commit()
1107
1105
1108 data = {
1106 data = {
1109 'pull_request_id': pull_request.pull_request_id,
1107 'pull_request_id': pull_request.pull_request_id,
1110 'close_status': status_lbl,
1108 'close_status': status_lbl,
1111 'closed': True,
1109 'closed': True,
1112 }
1110 }
1113 return data
1111 return data
@@ -1,2536 +1,2534 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import time
20 import time
23
21
24 import rhodecode
22 import rhodecode
25 from rhodecode.api import (
23 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
27 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
28 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
29 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger, rc_cache, channelstream
30 from rhodecode.lib import audit_logger, rc_cache, channelstream
33 from rhodecode.lib import repo_maintenance
31 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
33 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
36 HasRepoPermissionAnyApi)
34 HasRepoPermissionAnyApi)
37 from rhodecode.lib.celerylib.utils import get_task_id
35 from rhodecode.lib.celerylib.utils import get_task_id
38 from rhodecode.lib.utils2 import (
36 from rhodecode.lib.utils2 import (
39 str2bool, time_to_datetime, safe_str, safe_int)
37 str2bool, time_to_datetime, safe_str, safe_int)
40 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.exceptions import (
39 from rhodecode.lib.exceptions import (
42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
40 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
43 from rhodecode.lib.vcs import RepositoryError
41 from rhodecode.lib.vcs import RepositoryError
44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
42 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
45 from rhodecode.model.changeset_status import ChangesetStatusModel
43 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (
45 from rhodecode.model.db import (
48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
46 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
49 ChangesetComment)
47 ChangesetComment)
50 from rhodecode.model.permission import PermissionModel
48 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
52 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo import RepoModel
53 from rhodecode.model.scm import ScmModel, RepoList
51 from rhodecode.model.scm import ScmModel, RepoList
54 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
52 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
55 from rhodecode.model import validation_schema
53 from rhodecode.model import validation_schema
56 from rhodecode.model.validation_schema.schemas import repo_schema
54 from rhodecode.model.validation_schema.schemas import repo_schema
57
55
58 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
59
57
60
58
61 @jsonrpc_method()
59 @jsonrpc_method()
62 def get_repo(request, apiuser, repoid, cache=Optional(True)):
60 def get_repo(request, apiuser, repoid, cache=Optional(True)):
63 """
61 """
64 Gets an existing repository by its name or repository_id.
62 Gets an existing repository by its name or repository_id.
65
63
66 The members section so the output returns users groups or users
64 The members section so the output returns users groups or users
67 associated with that repository.
65 associated with that repository.
68
66
69 This command can only be run using an |authtoken| with admin rights,
67 This command can only be run using an |authtoken| with admin rights,
70 or users with at least read rights to the |repo|.
68 or users with at least read rights to the |repo|.
71
69
72 :param apiuser: This is filled automatically from the |authtoken|.
70 :param apiuser: This is filled automatically from the |authtoken|.
73 :type apiuser: AuthUser
71 :type apiuser: AuthUser
74 :param repoid: The repository name or repository id.
72 :param repoid: The repository name or repository id.
75 :type repoid: str or int
73 :type repoid: str or int
76 :param cache: use the cached value for last changeset
74 :param cache: use the cached value for last changeset
77 :type: cache: Optional(bool)
75 :type: cache: Optional(bool)
78
76
79 Example output:
77 Example output:
80
78
81 .. code-block:: bash
79 .. code-block:: bash
82
80
83 {
81 {
84 "error": null,
82 "error": null,
85 "id": <repo_id>,
83 "id": <repo_id>,
86 "result": {
84 "result": {
87 "clone_uri": null,
85 "clone_uri": null,
88 "created_on": "timestamp",
86 "created_on": "timestamp",
89 "description": "repo description",
87 "description": "repo description",
90 "enable_downloads": false,
88 "enable_downloads": false,
91 "enable_locking": false,
89 "enable_locking": false,
92 "enable_statistics": false,
90 "enable_statistics": false,
93 "followers": [
91 "followers": [
94 {
92 {
95 "active": true,
93 "active": true,
96 "admin": false,
94 "admin": false,
97 "api_key": "****************************************",
95 "api_key": "****************************************",
98 "api_keys": [
96 "api_keys": [
99 "****************************************"
97 "****************************************"
100 ],
98 ],
101 "email": "user@example.com",
99 "email": "user@example.com",
102 "emails": [
100 "emails": [
103 "user@example.com"
101 "user@example.com"
104 ],
102 ],
105 "extern_name": "rhodecode",
103 "extern_name": "rhodecode",
106 "extern_type": "rhodecode",
104 "extern_type": "rhodecode",
107 "firstname": "username",
105 "firstname": "username",
108 "ip_addresses": [],
106 "ip_addresses": [],
109 "language": null,
107 "language": null,
110 "last_login": "2015-09-16T17:16:35.854",
108 "last_login": "2015-09-16T17:16:35.854",
111 "lastname": "surname",
109 "lastname": "surname",
112 "user_id": <user_id>,
110 "user_id": <user_id>,
113 "username": "name"
111 "username": "name"
114 }
112 }
115 ],
113 ],
116 "fork_of": "parent-repo",
114 "fork_of": "parent-repo",
117 "landing_rev": [
115 "landing_rev": [
118 "rev",
116 "rev",
119 "tip"
117 "tip"
120 ],
118 ],
121 "last_changeset": {
119 "last_changeset": {
122 "author": "User <user@example.com>",
120 "author": "User <user@example.com>",
123 "branch": "default",
121 "branch": "default",
124 "date": "timestamp",
122 "date": "timestamp",
125 "message": "last commit message",
123 "message": "last commit message",
126 "parents": [
124 "parents": [
127 {
125 {
128 "raw_id": "commit-id"
126 "raw_id": "commit-id"
129 }
127 }
130 ],
128 ],
131 "raw_id": "commit-id",
129 "raw_id": "commit-id",
132 "revision": <revision number>,
130 "revision": <revision number>,
133 "short_id": "short id"
131 "short_id": "short id"
134 },
132 },
135 "lock_reason": null,
133 "lock_reason": null,
136 "locked_by": null,
134 "locked_by": null,
137 "locked_date": null,
135 "locked_date": null,
138 "owner": "owner-name",
136 "owner": "owner-name",
139 "permissions": [
137 "permissions": [
140 {
138 {
141 "name": "super-admin-name",
139 "name": "super-admin-name",
142 "origin": "super-admin",
140 "origin": "super-admin",
143 "permission": "repository.admin",
141 "permission": "repository.admin",
144 "type": "user"
142 "type": "user"
145 },
143 },
146 {
144 {
147 "name": "owner-name",
145 "name": "owner-name",
148 "origin": "owner",
146 "origin": "owner",
149 "permission": "repository.admin",
147 "permission": "repository.admin",
150 "type": "user"
148 "type": "user"
151 },
149 },
152 {
150 {
153 "name": "user-group-name",
151 "name": "user-group-name",
154 "origin": "permission",
152 "origin": "permission",
155 "permission": "repository.write",
153 "permission": "repository.write",
156 "type": "user_group"
154 "type": "user_group"
157 }
155 }
158 ],
156 ],
159 "private": true,
157 "private": true,
160 "repo_id": 676,
158 "repo_id": 676,
161 "repo_name": "user-group/repo-name",
159 "repo_name": "user-group/repo-name",
162 "repo_type": "hg"
160 "repo_type": "hg"
163 }
161 }
164 }
162 }
165 """
163 """
166
164
167 repo = get_repo_or_error(repoid)
165 repo = get_repo_or_error(repoid)
168 cache = Optional.extract(cache)
166 cache = Optional.extract(cache)
169
167
170 include_secrets = False
168 include_secrets = False
171 if has_superadmin_permission(apiuser):
169 if has_superadmin_permission(apiuser):
172 include_secrets = True
170 include_secrets = True
173 else:
171 else:
174 # check if we have at least read permission for this repo !
172 # check if we have at least read permission for this repo !
175 _perms = (
173 _perms = (
176 'repository.admin', 'repository.write', 'repository.read',)
174 'repository.admin', 'repository.write', 'repository.read',)
177 validate_repo_permissions(apiuser, repoid, repo, _perms)
175 validate_repo_permissions(apiuser, repoid, repo, _perms)
178
176
179 permissions = []
177 permissions = []
180 for _user in repo.permissions():
178 for _user in repo.permissions():
181 user_data = {
179 user_data = {
182 'name': _user.username,
180 'name': _user.username,
183 'permission': _user.permission,
181 'permission': _user.permission,
184 'origin': get_origin(_user),
182 'origin': get_origin(_user),
185 'type': "user",
183 'type': "user",
186 }
184 }
187 permissions.append(user_data)
185 permissions.append(user_data)
188
186
189 for _user_group in repo.permission_user_groups():
187 for _user_group in repo.permission_user_groups():
190 user_group_data = {
188 user_group_data = {
191 'name': _user_group.users_group_name,
189 'name': _user_group.users_group_name,
192 'permission': _user_group.permission,
190 'permission': _user_group.permission,
193 'origin': get_origin(_user_group),
191 'origin': get_origin(_user_group),
194 'type': "user_group",
192 'type': "user_group",
195 }
193 }
196 permissions.append(user_group_data)
194 permissions.append(user_group_data)
197
195
198 following_users = [
196 following_users = [
199 user.user.get_api_data(include_secrets=include_secrets)
197 user.user.get_api_data(include_secrets=include_secrets)
200 for user in repo.followers]
198 for user in repo.followers]
201
199
202 if not cache:
200 if not cache:
203 repo.update_commit_cache()
201 repo.update_commit_cache()
204 data = repo.get_api_data(include_secrets=include_secrets)
202 data = repo.get_api_data(include_secrets=include_secrets)
205 data['permissions'] = permissions
203 data['permissions'] = permissions
206 data['followers'] = following_users
204 data['followers'] = following_users
207
205
208 return data
206 return data
209
207
210
208
211 @jsonrpc_method()
209 @jsonrpc_method()
212 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
210 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
213 """
211 """
214 Lists all existing repositories.
212 Lists all existing repositories.
215
213
216 This command can only be run using an |authtoken| with admin rights,
214 This command can only be run using an |authtoken| with admin rights,
217 or users with at least read rights to |repos|.
215 or users with at least read rights to |repos|.
218
216
219 :param apiuser: This is filled automatically from the |authtoken|.
217 :param apiuser: This is filled automatically from the |authtoken|.
220 :type apiuser: AuthUser
218 :type apiuser: AuthUser
221 :param root: specify root repository group to fetch repositories.
219 :param root: specify root repository group to fetch repositories.
222 filters the returned repositories to be members of given root group.
220 filters the returned repositories to be members of given root group.
223 :type root: Optional(None)
221 :type root: Optional(None)
224 :param traverse: traverse given root into subrepositories. With this flag
222 :param traverse: traverse given root into subrepositories. With this flag
225 set to False, it will only return top-level repositories from `root`.
223 set to False, it will only return top-level repositories from `root`.
226 if root is empty it will return just top-level repositories.
224 if root is empty it will return just top-level repositories.
227 :type traverse: Optional(True)
225 :type traverse: Optional(True)
228
226
229
227
230 Example output:
228 Example output:
231
229
232 .. code-block:: bash
230 .. code-block:: bash
233
231
234 id : <id_given_in_input>
232 id : <id_given_in_input>
235 result: [
233 result: [
236 {
234 {
237 "repo_id" : "<repo_id>",
235 "repo_id" : "<repo_id>",
238 "repo_name" : "<reponame>"
236 "repo_name" : "<reponame>"
239 "repo_type" : "<repo_type>",
237 "repo_type" : "<repo_type>",
240 "clone_uri" : "<clone_uri>",
238 "clone_uri" : "<clone_uri>",
241 "private": : "<bool>",
239 "private": : "<bool>",
242 "created_on" : "<datetimecreated>",
240 "created_on" : "<datetimecreated>",
243 "description" : "<description>",
241 "description" : "<description>",
244 "landing_rev": "<landing_rev>",
242 "landing_rev": "<landing_rev>",
245 "owner": "<repo_owner>",
243 "owner": "<repo_owner>",
246 "fork_of": "<name_of_fork_parent>",
244 "fork_of": "<name_of_fork_parent>",
247 "enable_downloads": "<bool>",
245 "enable_downloads": "<bool>",
248 "enable_locking": "<bool>",
246 "enable_locking": "<bool>",
249 "enable_statistics": "<bool>",
247 "enable_statistics": "<bool>",
250 },
248 },
251 ...
249 ...
252 ]
250 ]
253 error: null
251 error: null
254 """
252 """
255
253
256 include_secrets = has_superadmin_permission(apiuser)
254 include_secrets = has_superadmin_permission(apiuser)
257 _perms = ('repository.read', 'repository.write', 'repository.admin',)
255 _perms = ('repository.read', 'repository.write', 'repository.admin',)
258 extras = {'user': apiuser}
256 extras = {'user': apiuser}
259
257
260 root = Optional.extract(root)
258 root = Optional.extract(root)
261 traverse = Optional.extract(traverse, binary=True)
259 traverse = Optional.extract(traverse, binary=True)
262
260
263 if root:
261 if root:
264 # verify parent existance, if it's empty return an error
262 # verify parent existance, if it's empty return an error
265 parent = RepoGroup.get_by_group_name(root)
263 parent = RepoGroup.get_by_group_name(root)
266 if not parent:
264 if not parent:
267 raise JSONRPCError(
265 raise JSONRPCError(
268 'Root repository group `{}` does not exist'.format(root))
266 f'Root repository group `{root}` does not exist')
269
267
270 if traverse:
268 if traverse:
271 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
269 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
272 else:
270 else:
273 repos = RepoModel().get_repos_for_root(root=parent)
271 repos = RepoModel().get_repos_for_root(root=parent)
274 else:
272 else:
275 if traverse:
273 if traverse:
276 repos = RepoModel().get_all()
274 repos = RepoModel().get_all()
277 else:
275 else:
278 # return just top-level
276 # return just top-level
279 repos = RepoModel().get_repos_for_root(root=None)
277 repos = RepoModel().get_repos_for_root(root=None)
280
278
281 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
279 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
282 return [repo.get_api_data(include_secrets=include_secrets)
280 return [repo.get_api_data(include_secrets=include_secrets)
283 for repo in repo_list]
281 for repo in repo_list]
284
282
285
283
286 @jsonrpc_method()
284 @jsonrpc_method()
287 def get_repo_changeset(request, apiuser, repoid, revision,
285 def get_repo_changeset(request, apiuser, repoid, revision,
288 details=Optional('basic')):
286 details=Optional('basic')):
289 """
287 """
290 Returns information about a changeset.
288 Returns information about a changeset.
291
289
292 Additionally parameters define the amount of details returned by
290 Additionally parameters define the amount of details returned by
293 this function.
291 this function.
294
292
295 This command can only be run using an |authtoken| with admin rights,
293 This command can only be run using an |authtoken| with admin rights,
296 or users with at least read rights to the |repo|.
294 or users with at least read rights to the |repo|.
297
295
298 :param apiuser: This is filled automatically from the |authtoken|.
296 :param apiuser: This is filled automatically from the |authtoken|.
299 :type apiuser: AuthUser
297 :type apiuser: AuthUser
300 :param repoid: The repository name or repository id
298 :param repoid: The repository name or repository id
301 :type repoid: str or int
299 :type repoid: str or int
302 :param revision: revision for which listing should be done
300 :param revision: revision for which listing should be done
303 :type revision: str
301 :type revision: str
304 :param details: details can be 'basic|extended|full' full gives diff
302 :param details: details can be 'basic|extended|full' full gives diff
305 info details like the diff itself, and number of changed files etc.
303 info details like the diff itself, and number of changed files etc.
306 :type details: Optional(str)
304 :type details: Optional(str)
307
305
308 """
306 """
309 repo = get_repo_or_error(repoid)
307 repo = get_repo_or_error(repoid)
310 if not has_superadmin_permission(apiuser):
308 if not has_superadmin_permission(apiuser):
311 _perms = ('repository.admin', 'repository.write', 'repository.read',)
309 _perms = ('repository.admin', 'repository.write', 'repository.read',)
312 validate_repo_permissions(apiuser, repoid, repo, _perms)
310 validate_repo_permissions(apiuser, repoid, repo, _perms)
313
311
314 changes_details = Optional.extract(details)
312 changes_details = Optional.extract(details)
315 _changes_details_types = ['basic', 'extended', 'full']
313 _changes_details_types = ['basic', 'extended', 'full']
316 if changes_details not in _changes_details_types:
314 if changes_details not in _changes_details_types:
317 raise JSONRPCError(
315 raise JSONRPCError(
318 'ret_type must be one of %s' % (
316 'ret_type must be one of %s' % (
319 ','.join(_changes_details_types)))
317 ','.join(_changes_details_types)))
320
318
321 vcs_repo = repo.scm_instance()
319 vcs_repo = repo.scm_instance()
322 pre_load = ['author', 'branch', 'date', 'message', 'parents',
320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
323 'status', '_commit', '_file_paths']
321 'status', '_commit', '_file_paths']
324
322
325 try:
323 try:
326 commit = repo.get_commit(commit_id=revision, pre_load=pre_load)
324 commit = repo.get_commit(commit_id=revision, pre_load=pre_load)
327 except TypeError as e:
325 except TypeError as e:
328 raise JSONRPCError(safe_str(e))
326 raise JSONRPCError(safe_str(e))
329 _cs_json = commit.__json__()
327 _cs_json = commit.__json__()
330 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
328 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
331 if changes_details == 'full':
329 if changes_details == 'full':
332 _cs_json['refs'] = commit._get_refs()
330 _cs_json['refs'] = commit._get_refs()
333 return _cs_json
331 return _cs_json
334
332
335
333
336 @jsonrpc_method()
334 @jsonrpc_method()
337 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
335 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
338 details=Optional('basic')):
336 details=Optional('basic')):
339 """
337 """
340 Returns a set of commits limited by the number starting
338 Returns a set of commits limited by the number starting
341 from the `start_rev` option.
339 from the `start_rev` option.
342
340
343 Additional parameters define the amount of details returned by this
341 Additional parameters define the amount of details returned by this
344 function.
342 function.
345
343
346 This command can only be run using an |authtoken| with admin rights,
344 This command can only be run using an |authtoken| with admin rights,
347 or users with at least read rights to |repos|.
345 or users with at least read rights to |repos|.
348
346
349 :param apiuser: This is filled automatically from the |authtoken|.
347 :param apiuser: This is filled automatically from the |authtoken|.
350 :type apiuser: AuthUser
348 :type apiuser: AuthUser
351 :param repoid: The repository name or repository ID.
349 :param repoid: The repository name or repository ID.
352 :type repoid: str or int
350 :type repoid: str or int
353 :param start_rev: The starting revision from where to get changesets.
351 :param start_rev: The starting revision from where to get changesets.
354 :type start_rev: str
352 :type start_rev: str
355 :param limit: Limit the number of commits to this amount
353 :param limit: Limit the number of commits to this amount
356 :type limit: str or int
354 :type limit: str or int
357 :param details: Set the level of detail returned. Valid option are:
355 :param details: Set the level of detail returned. Valid option are:
358 ``basic``, ``extended`` and ``full``.
356 ``basic``, ``extended`` and ``full``.
359 :type details: Optional(str)
357 :type details: Optional(str)
360
358
361 .. note::
359 .. note::
362
360
363 Setting the parameter `details` to the value ``full`` is extensive
361 Setting the parameter `details` to the value ``full`` is extensive
364 and returns details like the diff itself, and the number
362 and returns details like the diff itself, and the number
365 of changed files.
363 of changed files.
366
364
367 """
365 """
368 repo = get_repo_or_error(repoid)
366 repo = get_repo_or_error(repoid)
369 if not has_superadmin_permission(apiuser):
367 if not has_superadmin_permission(apiuser):
370 _perms = ('repository.admin', 'repository.write', 'repository.read',)
368 _perms = ('repository.admin', 'repository.write', 'repository.read',)
371 validate_repo_permissions(apiuser, repoid, repo, _perms)
369 validate_repo_permissions(apiuser, repoid, repo, _perms)
372
370
373 changes_details = Optional.extract(details)
371 changes_details = Optional.extract(details)
374 _changes_details_types = ['basic', 'extended', 'full']
372 _changes_details_types = ['basic', 'extended', 'full']
375 if changes_details not in _changes_details_types:
373 if changes_details not in _changes_details_types:
376 raise JSONRPCError(
374 raise JSONRPCError(
377 'ret_type must be one of %s' % (
375 'ret_type must be one of %s' % (
378 ','.join(_changes_details_types)))
376 ','.join(_changes_details_types)))
379
377
380 limit = int(limit)
378 limit = int(limit)
381 pre_load = ['author', 'branch', 'date', 'message', 'parents',
379 pre_load = ['author', 'branch', 'date', 'message', 'parents',
382 'status', '_commit', '_file_paths']
380 'status', '_commit', '_file_paths']
383
381
384 vcs_repo = repo.scm_instance()
382 vcs_repo = repo.scm_instance()
385 # SVN needs a special case to distinguish its index and commit id
383 # SVN needs a special case to distinguish its index and commit id
386 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
384 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
387 start_rev = vcs_repo.commit_ids[0]
385 start_rev = vcs_repo.commit_ids[0]
388
386
389 try:
387 try:
390 commits = vcs_repo.get_commits(
388 commits = vcs_repo.get_commits(
391 start_id=start_rev, pre_load=pre_load, translate_tags=False)
389 start_id=start_rev, pre_load=pre_load, translate_tags=False)
392 except TypeError as e:
390 except TypeError as e:
393 raise JSONRPCError(safe_str(e))
391 raise JSONRPCError(safe_str(e))
394 except Exception:
392 except Exception:
395 log.exception('Fetching of commits failed')
393 log.exception('Fetching of commits failed')
396 raise JSONRPCError('Error occurred during commit fetching')
394 raise JSONRPCError('Error occurred during commit fetching')
397
395
398 ret = []
396 ret = []
399 for cnt, commit in enumerate(commits):
397 for cnt, commit in enumerate(commits):
400 if cnt >= limit != -1:
398 if cnt >= limit != -1:
401 break
399 break
402 _cs_json = commit.__json__()
400 _cs_json = commit.__json__()
403 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
401 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
404 if changes_details == 'full':
402 if changes_details == 'full':
405 _cs_json['refs'] = {
403 _cs_json['refs'] = {
406 'branches': [commit.branch],
404 'branches': [commit.branch],
407 'bookmarks': getattr(commit, 'bookmarks', []),
405 'bookmarks': getattr(commit, 'bookmarks', []),
408 'tags': commit.tags
406 'tags': commit.tags
409 }
407 }
410 ret.append(_cs_json)
408 ret.append(_cs_json)
411 return ret
409 return ret
412
410
413
411
414 @jsonrpc_method()
412 @jsonrpc_method()
415 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
413 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
416 ret_type=Optional('all'), details=Optional('basic'),
414 ret_type=Optional('all'), details=Optional('basic'),
417 max_file_bytes=Optional(None)):
415 max_file_bytes=Optional(None)):
418 """
416 """
419 Returns a list of nodes and children in a flat list for a given
417 Returns a list of nodes and children in a flat list for a given
420 path at given revision.
418 path at given revision.
421
419
422 It's possible to specify ret_type to show only `files` or `dirs`.
420 It's possible to specify ret_type to show only `files` or `dirs`.
423
421
424 This command can only be run using an |authtoken| with admin rights,
422 This command can only be run using an |authtoken| with admin rights,
425 or users with at least read rights to |repos|.
423 or users with at least read rights to |repos|.
426
424
427 :param apiuser: This is filled automatically from the |authtoken|.
425 :param apiuser: This is filled automatically from the |authtoken|.
428 :type apiuser: AuthUser
426 :type apiuser: AuthUser
429 :param repoid: The repository name or repository ID.
427 :param repoid: The repository name or repository ID.
430 :type repoid: str or int
428 :type repoid: str or int
431 :param revision: The revision for which listing should be done.
429 :param revision: The revision for which listing should be done.
432 :type revision: str
430 :type revision: str
433 :param root_path: The path from which to start displaying.
431 :param root_path: The path from which to start displaying.
434 :type root_path: str
432 :type root_path: str
435 :param ret_type: Set the return type. Valid options are
433 :param ret_type: Set the return type. Valid options are
436 ``all`` (default), ``files`` and ``dirs``.
434 ``all`` (default), ``files`` and ``dirs``.
437 :type ret_type: Optional(str)
435 :type ret_type: Optional(str)
438 :param details: Returns extended information about nodes, such as
436 :param details: Returns extended information about nodes, such as
439 md5, binary, and or content.
437 md5, binary, and or content.
440 The valid options are ``basic`` and ``full``.
438 The valid options are ``basic`` and ``full``.
441 :type details: Optional(str)
439 :type details: Optional(str)
442 :param max_file_bytes: Only return file content under this file size bytes
440 :param max_file_bytes: Only return file content under this file size bytes
443 :type details: Optional(int)
441 :type details: Optional(int)
444
442
445 Example output:
443 Example output:
446
444
447 .. code-block:: bash
445 .. code-block:: bash
448
446
449 id : <id_given_in_input>
447 id : <id_given_in_input>
450 result: [
448 result: [
451 {
449 {
452 "binary": false,
450 "binary": false,
453 "content": "File line",
451 "content": "File line",
454 "extension": "md",
452 "extension": "md",
455 "lines": 2,
453 "lines": 2,
456 "md5": "059fa5d29b19c0657e384749480f6422",
454 "md5": "059fa5d29b19c0657e384749480f6422",
457 "mimetype": "text/x-minidsrc",
455 "mimetype": "text/x-minidsrc",
458 "name": "file.md",
456 "name": "file.md",
459 "size": 580,
457 "size": 580,
460 "type": "file"
458 "type": "file"
461 },
459 },
462 ...
460 ...
463 ]
461 ]
464 error: null
462 error: null
465 """
463 """
466
464
467 repo = get_repo_or_error(repoid)
465 repo = get_repo_or_error(repoid)
468 if not has_superadmin_permission(apiuser):
466 if not has_superadmin_permission(apiuser):
469 _perms = ('repository.admin', 'repository.write', 'repository.read',)
467 _perms = ('repository.admin', 'repository.write', 'repository.read',)
470 validate_repo_permissions(apiuser, repoid, repo, _perms)
468 validate_repo_permissions(apiuser, repoid, repo, _perms)
471
469
472 ret_type = Optional.extract(ret_type)
470 ret_type = Optional.extract(ret_type)
473 details = Optional.extract(details)
471 details = Optional.extract(details)
474 max_file_bytes = Optional.extract(max_file_bytes)
472 max_file_bytes = Optional.extract(max_file_bytes)
475
473
476 _extended_types = ['basic', 'full']
474 _extended_types = ['basic', 'full']
477 if details not in _extended_types:
475 if details not in _extended_types:
478 ret_types = ','.join(_extended_types)
476 ret_types = ','.join(_extended_types)
479 raise JSONRPCError(f'ret_type must be one of {ret_types}')
477 raise JSONRPCError(f'ret_type must be one of {ret_types}')
480
478
481 extended_info = False
479 extended_info = False
482 content = False
480 content = False
483 if details == 'basic':
481 if details == 'basic':
484 extended_info = True
482 extended_info = True
485
483
486 if details == 'full':
484 if details == 'full':
487 extended_info = content = True
485 extended_info = content = True
488
486
489 _map = {}
487 _map = {}
490 try:
488 try:
491 # check if repo is not empty by any chance, skip quicker if it is.
489 # check if repo is not empty by any chance, skip quicker if it is.
492 _scm = repo.scm_instance()
490 _scm = repo.scm_instance()
493 if _scm.is_empty():
491 if _scm.is_empty():
494 return []
492 return []
495
493
496 _d, _f = ScmModel().get_nodes(
494 _d, _f = ScmModel().get_nodes(
497 repo, revision, root_path, flat=False,
495 repo, revision, root_path, flat=False,
498 extended_info=extended_info, content=content,
496 extended_info=extended_info, content=content,
499 max_file_bytes=max_file_bytes)
497 max_file_bytes=max_file_bytes)
500
498
501 _map = {
499 _map = {
502 'all': _d + _f,
500 'all': _d + _f,
503 'files': _f,
501 'files': _f,
504 'dirs': _d,
502 'dirs': _d,
505 }
503 }
506
504
507 return _map[ret_type]
505 return _map[ret_type]
508 except KeyError:
506 except KeyError:
509 keys = ','.join(sorted(_map.keys()))
507 keys = ','.join(sorted(_map.keys()))
510 raise JSONRPCError(f'ret_type must be one of {keys}')
508 raise JSONRPCError(f'ret_type must be one of {keys}')
511 except Exception:
509 except Exception:
512 log.exception("Exception occurred while trying to get repo nodes")
510 log.exception("Exception occurred while trying to get repo nodes")
513 raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` nodes')
511 raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` nodes')
514
512
515
513
516 @jsonrpc_method()
514 @jsonrpc_method()
517 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
515 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
518 max_file_bytes=Optional(0), details=Optional('basic'),
516 max_file_bytes=Optional(0), details=Optional('basic'),
519 cache=Optional(True)):
517 cache=Optional(True)):
520 """
518 """
521 Returns a single file from repository at given revision.
519 Returns a single file from repository at given revision.
522
520
523 This command can only be run using an |authtoken| with admin rights,
521 This command can only be run using an |authtoken| with admin rights,
524 or users with at least read rights to |repos|.
522 or users with at least read rights to |repos|.
525
523
526 :param apiuser: This is filled automatically from the |authtoken|.
524 :param apiuser: This is filled automatically from the |authtoken|.
527 :type apiuser: AuthUser
525 :type apiuser: AuthUser
528 :param repoid: The repository name or repository ID.
526 :param repoid: The repository name or repository ID.
529 :type repoid: str or int
527 :type repoid: str or int
530 :param commit_id: The revision for which listing should be done.
528 :param commit_id: The revision for which listing should be done.
531 :type commit_id: str
529 :type commit_id: str
532 :param file_path: The path from which to start displaying.
530 :param file_path: The path from which to start displaying.
533 :type file_path: str
531 :type file_path: str
534 :param details: Returns different set of information about nodes.
532 :param details: Returns different set of information about nodes.
535 The valid options are ``minimal`` ``basic`` and ``full``.
533 The valid options are ``minimal`` ``basic`` and ``full``.
536 :type details: Optional(str)
534 :type details: Optional(str)
537 :param max_file_bytes: Only return file content under this file size bytes
535 :param max_file_bytes: Only return file content under this file size bytes
538 :type max_file_bytes: Optional(int)
536 :type max_file_bytes: Optional(int)
539 :param cache: Use internal caches for fetching files. If disabled fetching
537 :param cache: Use internal caches for fetching files. If disabled fetching
540 files is slower but more memory efficient
538 files is slower but more memory efficient
541 :type cache: Optional(bool)
539 :type cache: Optional(bool)
542
540
543 Example output:
541 Example output:
544
542
545 .. code-block:: bash
543 .. code-block:: bash
546
544
547 id : <id_given_in_input>
545 id : <id_given_in_input>
548 result: {
546 result: {
549 "binary": false,
547 "binary": false,
550 "extension": "py",
548 "extension": "py",
551 "lines": 35,
549 "lines": 35,
552 "content": "....",
550 "content": "....",
553 "md5": "76318336366b0f17ee249e11b0c99c41",
551 "md5": "76318336366b0f17ee249e11b0c99c41",
554 "mimetype": "text/x-python",
552 "mimetype": "text/x-python",
555 "name": "python.py",
553 "name": "python.py",
556 "size": 817,
554 "size": 817,
557 "type": "file",
555 "type": "file",
558 }
556 }
559 error: null
557 error: null
560 """
558 """
561
559
562 repo = get_repo_or_error(repoid)
560 repo = get_repo_or_error(repoid)
563 if not has_superadmin_permission(apiuser):
561 if not has_superadmin_permission(apiuser):
564 _perms = ('repository.admin', 'repository.write', 'repository.read',)
562 _perms = ('repository.admin', 'repository.write', 'repository.read',)
565 validate_repo_permissions(apiuser, repoid, repo, _perms)
563 validate_repo_permissions(apiuser, repoid, repo, _perms)
566
564
567 cache = Optional.extract(cache, binary=True)
565 cache = Optional.extract(cache, binary=True)
568 details = Optional.extract(details)
566 details = Optional.extract(details)
569 max_file_bytes = Optional.extract(max_file_bytes)
567 max_file_bytes = Optional.extract(max_file_bytes)
570
568
571 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
569 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
572 if details not in _extended_types:
570 if details not in _extended_types:
573 ret_types = ','.join(_extended_types)
571 ret_types = ','.join(_extended_types)
574 raise JSONRPCError(f'ret_type must be one of %s, got {ret_types}', details)
572 raise JSONRPCError(f'ret_type must be one of %s, got {ret_types}', details)
575 extended_info = False
573 extended_info = False
576 content = False
574 content = False
577
575
578 if details == 'minimal':
576 if details == 'minimal':
579 extended_info = False
577 extended_info = False
580
578
581 elif details == 'basic':
579 elif details == 'basic':
582 extended_info = True
580 extended_info = True
583
581
584 elif details == 'full':
582 elif details == 'full':
585 extended_info = content = True
583 extended_info = content = True
586
584
587 file_path = safe_str(file_path)
585 file_path = safe_str(file_path)
588 try:
586 try:
589 # check if repo is not empty by any chance, skip quicker if it is.
587 # check if repo is not empty by any chance, skip quicker if it is.
590 _scm = repo.scm_instance()
588 _scm = repo.scm_instance()
591 if _scm.is_empty():
589 if _scm.is_empty():
592 return None
590 return None
593
591
594 node = ScmModel().get_node(
592 node = ScmModel().get_node(
595 repo, commit_id, file_path, extended_info=extended_info,
593 repo, commit_id, file_path, extended_info=extended_info,
596 content=content, max_file_bytes=max_file_bytes, cache=cache)
594 content=content, max_file_bytes=max_file_bytes, cache=cache)
597
595
598 except NodeDoesNotExistError:
596 except NodeDoesNotExistError:
599 raise JSONRPCError(
597 raise JSONRPCError(
600 f'There is no file in repo: `{repo.repo_name}` at path `{file_path}` for commit: `{commit_id}`')
598 f'There is no file in repo: `{repo.repo_name}` at path `{file_path}` for commit: `{commit_id}`')
601 except Exception:
599 except Exception:
602 log.exception("Exception occurred while trying to get repo %s file",
600 log.exception("Exception occurred while trying to get repo %s file",
603 repo.repo_name)
601 repo.repo_name)
604 raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` file at path {file_path}')
602 raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` file at path {file_path}')
605
603
606 return node
604 return node
607
605
608
606
609 @jsonrpc_method()
607 @jsonrpc_method()
610 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
608 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
611 """
609 """
612 Returns a list of tree nodes for path at given revision. This api is built
610 Returns a list of tree nodes for path at given revision. This api is built
613 strictly for usage in full text search building, and shouldn't be consumed
611 strictly for usage in full text search building, and shouldn't be consumed
614
612
615 This command can only be run using an |authtoken| with admin rights,
613 This command can only be run using an |authtoken| with admin rights,
616 or users with at least read rights to |repos|.
614 or users with at least read rights to |repos|.
617
615
618 """
616 """
619
617
620 repo = get_repo_or_error(repoid)
618 repo = get_repo_or_error(repoid)
621 if not has_superadmin_permission(apiuser):
619 if not has_superadmin_permission(apiuser):
622 _perms = ('repository.admin', 'repository.write', 'repository.read',)
620 _perms = ('repository.admin', 'repository.write', 'repository.read',)
623 validate_repo_permissions(apiuser, repoid, repo, _perms)
621 validate_repo_permissions(apiuser, repoid, repo, _perms)
624
622
625 repo_id = repo.repo_id
623 repo_id = repo.repo_id
626 cache_seconds = rhodecode.ConfigGet().get_int('rc_cache.cache_repo.expiration_time')
624 cache_seconds = rhodecode.ConfigGet().get_int('rc_cache.cache_repo.expiration_time')
627 cache_on = cache_seconds > 0
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 rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
628 rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
631
629
632 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
630 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
633 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
631 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
634
632
635 try:
633 try:
636 # check if repo is not empty by any chance, skip quicker if it is.
634 # check if repo is not empty by any chance, skip quicker if it is.
637 _scm = repo.scm_instance()
635 _scm = repo.scm_instance()
638 if not _scm or _scm.is_empty():
636 if not _scm or _scm.is_empty():
639 return []
637 return []
640 except RepositoryError:
638 except RepositoryError:
641 log.exception("Exception occurred while trying to get repo nodes")
639 log.exception("Exception occurred while trying to get repo nodes")
642 raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` nodes')
640 raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` nodes')
643
641
644 try:
642 try:
645 # we need to resolve commit_id to a FULL sha for cache to work correctly.
643 # we need to resolve commit_id to a FULL sha for cache to work correctly.
646 # sending 'master' is a pointer that needs to be translated to current commit.
644 # sending 'master' is a pointer that needs to be translated to current commit.
647 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
645 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
648 log.debug(
646 log.debug(
649 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
647 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
650 'with caching: %s[TTL: %ss]' % (
648 'with caching: %s[TTL: %ss]' % (
651 repo_id, commit_id, cache_on, cache_seconds or 0))
649 repo_id, commit_id, cache_on, cache_seconds or 0))
652
650
653 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
651 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
654
652
655 return tree_files
653 return tree_files
656
654
657 except Exception:
655 except Exception:
658 log.exception("Exception occurred while trying to get repo nodes")
656 log.exception("Exception occurred while trying to get repo nodes")
659 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
657 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
660
658
661
659
662 @jsonrpc_method()
660 @jsonrpc_method()
663 def get_repo_refs(request, apiuser, repoid):
661 def get_repo_refs(request, apiuser, repoid):
664 """
662 """
665 Returns a dictionary of current references. It returns
663 Returns a dictionary of current references. It returns
666 bookmarks, branches, closed_branches, and tags for given repository
664 bookmarks, branches, closed_branches, and tags for given repository
667
665
668 It's possible to specify ret_type to show only `files` or `dirs`.
666 It's possible to specify ret_type to show only `files` or `dirs`.
669
667
670 This command can only be run using an |authtoken| with admin rights,
668 This command can only be run using an |authtoken| with admin rights,
671 or users with at least read rights to |repos|.
669 or users with at least read rights to |repos|.
672
670
673 :param apiuser: This is filled automatically from the |authtoken|.
671 :param apiuser: This is filled automatically from the |authtoken|.
674 :type apiuser: AuthUser
672 :type apiuser: AuthUser
675 :param repoid: The repository name or repository ID.
673 :param repoid: The repository name or repository ID.
676 :type repoid: str or int
674 :type repoid: str or int
677
675
678 Example output:
676 Example output:
679
677
680 .. code-block:: bash
678 .. code-block:: bash
681
679
682 id : <id_given_in_input>
680 id : <id_given_in_input>
683 "result": {
681 "result": {
684 "bookmarks": {
682 "bookmarks": {
685 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
683 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
686 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
684 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
687 },
685 },
688 "branches": {
686 "branches": {
689 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
687 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
690 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
688 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
691 },
689 },
692 "branches_closed": {},
690 "branches_closed": {},
693 "tags": {
691 "tags": {
694 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
692 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
695 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
693 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
696 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
694 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
697 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
695 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
698 }
696 }
699 }
697 }
700 error: null
698 error: null
701 """
699 """
702
700
703 repo = get_repo_or_error(repoid)
701 repo = get_repo_or_error(repoid)
704 if not has_superadmin_permission(apiuser):
702 if not has_superadmin_permission(apiuser):
705 _perms = ('repository.admin', 'repository.write', 'repository.read',)
703 _perms = ('repository.admin', 'repository.write', 'repository.read',)
706 validate_repo_permissions(apiuser, repoid, repo, _perms)
704 validate_repo_permissions(apiuser, repoid, repo, _perms)
707
705
708 try:
706 try:
709 # check if repo is not empty by any chance, skip quicker if it is.
707 # check if repo is not empty by any chance, skip quicker if it is.
710 vcs_instance = repo.scm_instance()
708 vcs_instance = repo.scm_instance()
711 refs = vcs_instance.refs()
709 refs = vcs_instance.refs()
712 return refs
710 return refs
713 except Exception:
711 except Exception:
714 log.exception("Exception occurred while trying to get repo refs")
712 log.exception("Exception occurred while trying to get repo refs")
715 raise JSONRPCError(
713 raise JSONRPCError(
716 'failed to get repo: `%s` references' % repo.repo_name
714 'failed to get repo: `%s` references' % repo.repo_name
717 )
715 )
718
716
719
717
720 @jsonrpc_method()
718 @jsonrpc_method()
721 def create_repo(
719 def create_repo(
722 request, apiuser, repo_name, repo_type,
720 request, apiuser, repo_name, repo_type,
723 owner=Optional(OAttr('apiuser')),
721 owner=Optional(OAttr('apiuser')),
724 description=Optional(''),
722 description=Optional(''),
725 private=Optional(False),
723 private=Optional(False),
726 clone_uri=Optional(None),
724 clone_uri=Optional(None),
727 push_uri=Optional(None),
725 push_uri=Optional(None),
728 landing_rev=Optional(None),
726 landing_rev=Optional(None),
729 enable_statistics=Optional(False),
727 enable_statistics=Optional(False),
730 enable_locking=Optional(False),
728 enable_locking=Optional(False),
731 enable_downloads=Optional(False),
729 enable_downloads=Optional(False),
732 copy_permissions=Optional(False)):
730 copy_permissions=Optional(False)):
733 """
731 """
734 Creates a repository.
732 Creates a repository.
735
733
736 * If the repository name contains "/", repository will be created inside
734 * If the repository name contains "/", repository will be created inside
737 a repository group or nested repository groups
735 a repository group or nested repository groups
738
736
739 For example "foo/bar/repo1" will create |repo| called "repo1" inside
737 For example "foo/bar/repo1" will create |repo| called "repo1" inside
740 group "foo/bar". You have to have permissions to access and write to
738 group "foo/bar". You have to have permissions to access and write to
741 the last repository group ("bar" in this example)
739 the last repository group ("bar" in this example)
742
740
743 This command can only be run using an |authtoken| with at least
741 This command can only be run using an |authtoken| with at least
744 permissions to create repositories, or write permissions to
742 permissions to create repositories, or write permissions to
745 parent repository groups.
743 parent repository groups.
746
744
747 :param apiuser: This is filled automatically from the |authtoken|.
745 :param apiuser: This is filled automatically from the |authtoken|.
748 :type apiuser: AuthUser
746 :type apiuser: AuthUser
749 :param repo_name: Set the repository name.
747 :param repo_name: Set the repository name.
750 :type repo_name: str
748 :type repo_name: str
751 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
749 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
752 :type repo_type: str
750 :type repo_type: str
753 :param owner: user_id or username
751 :param owner: user_id or username
754 :type owner: Optional(str)
752 :type owner: Optional(str)
755 :param description: Set the repository description.
753 :param description: Set the repository description.
756 :type description: Optional(str)
754 :type description: Optional(str)
757 :param private: set repository as private
755 :param private: set repository as private
758 :type private: bool
756 :type private: bool
759 :param clone_uri: set clone_uri
757 :param clone_uri: set clone_uri
760 :type clone_uri: str
758 :type clone_uri: str
761 :param push_uri: set push_uri
759 :param push_uri: set push_uri
762 :type push_uri: str
760 :type push_uri: str
763 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
761 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
764 :type landing_rev: str
762 :type landing_rev: str
765 :param enable_locking:
763 :param enable_locking:
766 :type enable_locking: bool
764 :type enable_locking: bool
767 :param enable_downloads:
765 :param enable_downloads:
768 :type enable_downloads: bool
766 :type enable_downloads: bool
769 :param enable_statistics:
767 :param enable_statistics:
770 :type enable_statistics: bool
768 :type enable_statistics: bool
771 :param copy_permissions: Copy permission from group in which the
769 :param copy_permissions: Copy permission from group in which the
772 repository is being created.
770 repository is being created.
773 :type copy_permissions: bool
771 :type copy_permissions: bool
774
772
775
773
776 Example output:
774 Example output:
777
775
778 .. code-block:: bash
776 .. code-block:: bash
779
777
780 id : <id_given_in_input>
778 id : <id_given_in_input>
781 result: {
779 result: {
782 "msg": "Created new repository `<reponame>`",
780 "msg": "Created new repository `<reponame>`",
783 "success": true,
781 "success": true,
784 "task": "<celery task id or None if done sync>"
782 "task": "<celery task id or None if done sync>"
785 }
783 }
786 error: null
784 error: null
787
785
788
786
789 Example error output:
787 Example error output:
790
788
791 .. code-block:: bash
789 .. code-block:: bash
792
790
793 id : <id_given_in_input>
791 id : <id_given_in_input>
794 result : null
792 result : null
795 error : {
793 error : {
796 'failed to create repository `<repo_name>`'
794 'failed to create repository `<repo_name>`'
797 }
795 }
798
796
799 """
797 """
800
798
801 owner = validate_set_owner_permissions(apiuser, owner)
799 owner = validate_set_owner_permissions(apiuser, owner)
802
800
803 description = Optional.extract(description)
801 description = Optional.extract(description)
804 copy_permissions = Optional.extract(copy_permissions)
802 copy_permissions = Optional.extract(copy_permissions)
805 clone_uri = Optional.extract(clone_uri)
803 clone_uri = Optional.extract(clone_uri)
806 push_uri = Optional.extract(push_uri)
804 push_uri = Optional.extract(push_uri)
807
805
808 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
806 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
809 if isinstance(private, Optional):
807 if isinstance(private, Optional):
810 private = defs.get('repo_private') or Optional.extract(private)
808 private = defs.get('repo_private') or Optional.extract(private)
811 if isinstance(repo_type, Optional):
809 if isinstance(repo_type, Optional):
812 repo_type = defs.get('repo_type')
810 repo_type = defs.get('repo_type')
813 if isinstance(enable_statistics, Optional):
811 if isinstance(enable_statistics, Optional):
814 enable_statistics = defs.get('repo_enable_statistics')
812 enable_statistics = defs.get('repo_enable_statistics')
815 if isinstance(enable_locking, Optional):
813 if isinstance(enable_locking, Optional):
816 enable_locking = defs.get('repo_enable_locking')
814 enable_locking = defs.get('repo_enable_locking')
817 if isinstance(enable_downloads, Optional):
815 if isinstance(enable_downloads, Optional):
818 enable_downloads = defs.get('repo_enable_downloads')
816 enable_downloads = defs.get('repo_enable_downloads')
819
817
820 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
818 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
821 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
819 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
822 ref_choices = list(set(ref_choices + [landing_ref]))
820 ref_choices = list(set(ref_choices + [landing_ref]))
823
821
824 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
822 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
825
823
826 schema = repo_schema.RepoSchema().bind(
824 schema = repo_schema.RepoSchema().bind(
827 repo_type_options=rhodecode.BACKENDS.keys(),
825 repo_type_options=rhodecode.BACKENDS.keys(),
828 repo_ref_options=ref_choices,
826 repo_ref_options=ref_choices,
829 repo_type=repo_type,
827 repo_type=repo_type,
830 # user caller
828 # user caller
831 user=apiuser)
829 user=apiuser)
832
830
833 try:
831 try:
834 schema_data = schema.deserialize(dict(
832 schema_data = schema.deserialize(dict(
835 repo_name=repo_name,
833 repo_name=repo_name,
836 repo_type=repo_type,
834 repo_type=repo_type,
837 repo_owner=owner.username,
835 repo_owner=owner.username,
838 repo_description=description,
836 repo_description=description,
839 repo_landing_commit_ref=landing_commit_ref,
837 repo_landing_commit_ref=landing_commit_ref,
840 repo_clone_uri=clone_uri,
838 repo_clone_uri=clone_uri,
841 repo_push_uri=push_uri,
839 repo_push_uri=push_uri,
842 repo_private=private,
840 repo_private=private,
843 repo_copy_permissions=copy_permissions,
841 repo_copy_permissions=copy_permissions,
844 repo_enable_statistics=enable_statistics,
842 repo_enable_statistics=enable_statistics,
845 repo_enable_downloads=enable_downloads,
843 repo_enable_downloads=enable_downloads,
846 repo_enable_locking=enable_locking))
844 repo_enable_locking=enable_locking))
847 except validation_schema.Invalid as err:
845 except validation_schema.Invalid as err:
848 raise JSONRPCValidationError(colander_exc=err)
846 raise JSONRPCValidationError(colander_exc=err)
849
847
850 try:
848 try:
851 data = {
849 data = {
852 'owner': owner,
850 'owner': owner,
853 'repo_name': schema_data['repo_group']['repo_name_without_group'],
851 'repo_name': schema_data['repo_group']['repo_name_without_group'],
854 'repo_name_full': schema_data['repo_name'],
852 'repo_name_full': schema_data['repo_name'],
855 'repo_group': schema_data['repo_group']['repo_group_id'],
853 'repo_group': schema_data['repo_group']['repo_group_id'],
856 'repo_type': schema_data['repo_type'],
854 'repo_type': schema_data['repo_type'],
857 'repo_description': schema_data['repo_description'],
855 'repo_description': schema_data['repo_description'],
858 'repo_private': schema_data['repo_private'],
856 'repo_private': schema_data['repo_private'],
859 'clone_uri': schema_data['repo_clone_uri'],
857 'clone_uri': schema_data['repo_clone_uri'],
860 'push_uri': schema_data['repo_push_uri'],
858 'push_uri': schema_data['repo_push_uri'],
861 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
859 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
862 'enable_statistics': schema_data['repo_enable_statistics'],
860 'enable_statistics': schema_data['repo_enable_statistics'],
863 'enable_locking': schema_data['repo_enable_locking'],
861 'enable_locking': schema_data['repo_enable_locking'],
864 'enable_downloads': schema_data['repo_enable_downloads'],
862 'enable_downloads': schema_data['repo_enable_downloads'],
865 'repo_copy_permissions': schema_data['repo_copy_permissions'],
863 'repo_copy_permissions': schema_data['repo_copy_permissions'],
866 }
864 }
867
865
868 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
866 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
869 task_id = get_task_id(task)
867 task_id = get_task_id(task)
870 # no commit, it's done in RepoModel, or async via celery
868 # no commit, it's done in RepoModel, or async via celery
871 return {
869 return {
872 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
870 'msg': "Created new repository `{}`".format(schema_data['repo_name']),
873 'success': True, # cannot return the repo data here since fork
871 'success': True, # cannot return the repo data here since fork
874 # can be done async
872 # can be done async
875 'task': task_id
873 'task': task_id
876 }
874 }
877 except Exception:
875 except Exception:
878 log.exception(
876 log.exception(
879 u"Exception while trying to create the repository %s",
877 "Exception while trying to create the repository %s",
880 schema_data['repo_name'])
878 schema_data['repo_name'])
881 raise JSONRPCError(
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 @jsonrpc_method()
883 @jsonrpc_method()
886 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
884 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
887 description=Optional('')):
885 description=Optional('')):
888 """
886 """
889 Adds an extra field to a repository.
887 Adds an extra field to a repository.
890
888
891 This command can only be run using an |authtoken| with at least
889 This command can only be run using an |authtoken| with at least
892 write permissions to the |repo|.
890 write permissions to the |repo|.
893
891
894 :param apiuser: This is filled automatically from the |authtoken|.
892 :param apiuser: This is filled automatically from the |authtoken|.
895 :type apiuser: AuthUser
893 :type apiuser: AuthUser
896 :param repoid: Set the repository name or repository id.
894 :param repoid: Set the repository name or repository id.
897 :type repoid: str or int
895 :type repoid: str or int
898 :param key: Create a unique field key for this repository.
896 :param key: Create a unique field key for this repository.
899 :type key: str
897 :type key: str
900 :param label:
898 :param label:
901 :type label: Optional(str)
899 :type label: Optional(str)
902 :param description:
900 :param description:
903 :type description: Optional(str)
901 :type description: Optional(str)
904 """
902 """
905 repo = get_repo_or_error(repoid)
903 repo = get_repo_or_error(repoid)
906 if not has_superadmin_permission(apiuser):
904 if not has_superadmin_permission(apiuser):
907 _perms = ('repository.admin',)
905 _perms = ('repository.admin',)
908 validate_repo_permissions(apiuser, repoid, repo, _perms)
906 validate_repo_permissions(apiuser, repoid, repo, _perms)
909
907
910 label = Optional.extract(label) or key
908 label = Optional.extract(label) or key
911 description = Optional.extract(description)
909 description = Optional.extract(description)
912
910
913 field = RepositoryField.get_by_key_name(key, repo)
911 field = RepositoryField.get_by_key_name(key, repo)
914 if field:
912 if field:
915 raise JSONRPCError('Field with key '
913 raise JSONRPCError('Field with key '
916 '`%s` exists for repo `%s`' % (key, repoid))
914 '`%s` exists for repo `%s`' % (key, repoid))
917
915
918 try:
916 try:
919 RepoModel().add_repo_field(repo, key, field_label=label,
917 RepoModel().add_repo_field(repo, key, field_label=label,
920 field_desc=description)
918 field_desc=description)
921 Session().commit()
919 Session().commit()
922 return {
920 return {
923 'msg': "Added new repository field `%s`" % (key,),
921 'msg': "Added new repository field `{}`".format(key),
924 'success': True,
922 'success': True,
925 }
923 }
926 except Exception:
924 except Exception:
927 log.exception("Exception occurred while trying to add field to repo")
925 log.exception("Exception occurred while trying to add field to repo")
928 raise JSONRPCError(
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 @jsonrpc_method()
930 @jsonrpc_method()
933 def remove_field_from_repo(request, apiuser, repoid, key):
931 def remove_field_from_repo(request, apiuser, repoid, key):
934 """
932 """
935 Removes an extra field from a repository.
933 Removes an extra field from a repository.
936
934
937 This command can only be run using an |authtoken| with at least
935 This command can only be run using an |authtoken| with at least
938 write permissions to the |repo|.
936 write permissions to the |repo|.
939
937
940 :param apiuser: This is filled automatically from the |authtoken|.
938 :param apiuser: This is filled automatically from the |authtoken|.
941 :type apiuser: AuthUser
939 :type apiuser: AuthUser
942 :param repoid: Set the repository name or repository ID.
940 :param repoid: Set the repository name or repository ID.
943 :type repoid: str or int
941 :type repoid: str or int
944 :param key: Set the unique field key for this repository.
942 :param key: Set the unique field key for this repository.
945 :type key: str
943 :type key: str
946 """
944 """
947
945
948 repo = get_repo_or_error(repoid)
946 repo = get_repo_or_error(repoid)
949 if not has_superadmin_permission(apiuser):
947 if not has_superadmin_permission(apiuser):
950 _perms = ('repository.admin',)
948 _perms = ('repository.admin',)
951 validate_repo_permissions(apiuser, repoid, repo, _perms)
949 validate_repo_permissions(apiuser, repoid, repo, _perms)
952
950
953 field = RepositoryField.get_by_key_name(key, repo)
951 field = RepositoryField.get_by_key_name(key, repo)
954 if not field:
952 if not field:
955 raise JSONRPCError('Field with key `%s` does not '
953 raise JSONRPCError('Field with key `%s` does not '
956 'exists for repo `%s`' % (key, repoid))
954 'exists for repo `%s`' % (key, repoid))
957
955
958 try:
956 try:
959 RepoModel().delete_repo_field(repo, field_key=key)
957 RepoModel().delete_repo_field(repo, field_key=key)
960 Session().commit()
958 Session().commit()
961 return {
959 return {
962 'msg': "Deleted repository field `%s`" % (key,),
960 'msg': "Deleted repository field `{}`".format(key),
963 'success': True,
961 'success': True,
964 }
962 }
965 except Exception:
963 except Exception:
966 log.exception(
964 log.exception(
967 "Exception occurred while trying to delete field from repo")
965 "Exception occurred while trying to delete field from repo")
968 raise JSONRPCError(
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 @jsonrpc_method()
970 @jsonrpc_method()
973 def update_repo(
971 def update_repo(
974 request, apiuser, repoid, repo_name=Optional(None),
972 request, apiuser, repoid, repo_name=Optional(None),
975 owner=Optional(OAttr('apiuser')), description=Optional(''),
973 owner=Optional(OAttr('apiuser')), description=Optional(''),
976 private=Optional(False),
974 private=Optional(False),
977 clone_uri=Optional(None), push_uri=Optional(None),
975 clone_uri=Optional(None), push_uri=Optional(None),
978 landing_rev=Optional(None), fork_of=Optional(None),
976 landing_rev=Optional(None), fork_of=Optional(None),
979 enable_statistics=Optional(False),
977 enable_statistics=Optional(False),
980 enable_locking=Optional(False),
978 enable_locking=Optional(False),
981 enable_downloads=Optional(False), fields=Optional('')):
979 enable_downloads=Optional(False), fields=Optional('')):
982 """
980 r"""
983 Updates a repository with the given information.
981 Updates a repository with the given information.
984
982
985 This command can only be run using an |authtoken| with at least
983 This command can only be run using an |authtoken| with at least
986 admin permissions to the |repo|.
984 admin permissions to the |repo|.
987
985
988 * If the repository name contains "/", repository will be updated
986 * If the repository name contains "/", repository will be updated
989 accordingly with a repository group or nested repository groups
987 accordingly with a repository group or nested repository groups
990
988
991 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
989 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
992 called "repo-test" and place it inside group "foo/bar".
990 called "repo-test" and place it inside group "foo/bar".
993 You have to have permissions to access and write to the last repository
991 You have to have permissions to access and write to the last repository
994 group ("bar" in this example)
992 group ("bar" in this example)
995
993
996 :param apiuser: This is filled automatically from the |authtoken|.
994 :param apiuser: This is filled automatically from the |authtoken|.
997 :type apiuser: AuthUser
995 :type apiuser: AuthUser
998 :param repoid: repository name or repository ID.
996 :param repoid: repository name or repository ID.
999 :type repoid: str or int
997 :type repoid: str or int
1000 :param repo_name: Update the |repo| name, including the
998 :param repo_name: Update the |repo| name, including the
1001 repository group it's in.
999 repository group it's in.
1002 :type repo_name: str
1000 :type repo_name: str
1003 :param owner: Set the |repo| owner.
1001 :param owner: Set the |repo| owner.
1004 :type owner: str
1002 :type owner: str
1005 :param fork_of: Set the |repo| as fork of another |repo|.
1003 :param fork_of: Set the |repo| as fork of another |repo|.
1006 :type fork_of: str
1004 :type fork_of: str
1007 :param description: Update the |repo| description.
1005 :param description: Update the |repo| description.
1008 :type description: str
1006 :type description: str
1009 :param private: Set the |repo| as private. (True | False)
1007 :param private: Set the |repo| as private. (True | False)
1010 :type private: bool
1008 :type private: bool
1011 :param clone_uri: Update the |repo| clone URI.
1009 :param clone_uri: Update the |repo| clone URI.
1012 :type clone_uri: str
1010 :type clone_uri: str
1013 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1011 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1014 :type landing_rev: str
1012 :type landing_rev: str
1015 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1013 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1016 :type enable_statistics: bool
1014 :type enable_statistics: bool
1017 :param enable_locking: Enable |repo| locking.
1015 :param enable_locking: Enable |repo| locking.
1018 :type enable_locking: bool
1016 :type enable_locking: bool
1019 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1017 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1020 :type enable_downloads: bool
1018 :type enable_downloads: bool
1021 :param fields: Add extra fields to the |repo|. Use the following
1019 :param fields: Add extra fields to the |repo|. Use the following
1022 example format: ``field_key=field_val,field_key2=fieldval2``.
1020 example format: ``field_key=field_val,field_key2=fieldval2``.
1023 Escape ', ' with \,
1021 Escape ', ' with \,
1024 :type fields: str
1022 :type fields: str
1025 """
1023 """
1026
1024
1027 repo = get_repo_or_error(repoid)
1025 repo = get_repo_or_error(repoid)
1028
1026
1029 include_secrets = False
1027 include_secrets = False
1030 if not has_superadmin_permission(apiuser):
1028 if not has_superadmin_permission(apiuser):
1031 _perms = ('repository.admin',)
1029 _perms = ('repository.admin',)
1032 validate_repo_permissions(apiuser, repoid, repo, _perms)
1030 validate_repo_permissions(apiuser, repoid, repo, _perms)
1033 else:
1031 else:
1034 include_secrets = True
1032 include_secrets = True
1035
1033
1036 updates = dict(
1034 updates = dict(
1037 repo_name=repo_name
1035 repo_name=repo_name
1038 if not isinstance(repo_name, Optional) else repo.repo_name,
1036 if not isinstance(repo_name, Optional) else repo.repo_name,
1039
1037
1040 fork_id=fork_of
1038 fork_id=fork_of
1041 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1039 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1042
1040
1043 user=owner
1041 user=owner
1044 if not isinstance(owner, Optional) else repo.user.username,
1042 if not isinstance(owner, Optional) else repo.user.username,
1045
1043
1046 repo_description=description
1044 repo_description=description
1047 if not isinstance(description, Optional) else repo.description,
1045 if not isinstance(description, Optional) else repo.description,
1048
1046
1049 repo_private=private
1047 repo_private=private
1050 if not isinstance(private, Optional) else repo.private,
1048 if not isinstance(private, Optional) else repo.private,
1051
1049
1052 clone_uri=clone_uri
1050 clone_uri=clone_uri
1053 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1051 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1054
1052
1055 push_uri=push_uri
1053 push_uri=push_uri
1056 if not isinstance(push_uri, Optional) else repo.push_uri,
1054 if not isinstance(push_uri, Optional) else repo.push_uri,
1057
1055
1058 repo_landing_rev=landing_rev
1056 repo_landing_rev=landing_rev
1059 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1057 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1060
1058
1061 repo_enable_statistics=enable_statistics
1059 repo_enable_statistics=enable_statistics
1062 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1060 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1063
1061
1064 repo_enable_locking=enable_locking
1062 repo_enable_locking=enable_locking
1065 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1063 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1066
1064
1067 repo_enable_downloads=enable_downloads
1065 repo_enable_downloads=enable_downloads
1068 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1066 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1069
1067
1070 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1068 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1071 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1069 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1072 request.translate, repo=repo)
1070 request.translate, repo=repo)
1073 ref_choices = list(set(ref_choices + [landing_ref]))
1071 ref_choices = list(set(ref_choices + [landing_ref]))
1074
1072
1075 old_values = repo.get_api_data()
1073 old_values = repo.get_api_data()
1076 repo_type = repo.repo_type
1074 repo_type = repo.repo_type
1077 schema = repo_schema.RepoSchema().bind(
1075 schema = repo_schema.RepoSchema().bind(
1078 repo_type_options=rhodecode.BACKENDS.keys(),
1076 repo_type_options=rhodecode.BACKENDS.keys(),
1079 repo_ref_options=ref_choices,
1077 repo_ref_options=ref_choices,
1080 repo_type=repo_type,
1078 repo_type=repo_type,
1081 # user caller
1079 # user caller
1082 user=apiuser,
1080 user=apiuser,
1083 old_values=old_values)
1081 old_values=old_values)
1084 try:
1082 try:
1085 schema_data = schema.deserialize(dict(
1083 schema_data = schema.deserialize(dict(
1086 # we save old value, users cannot change type
1084 # we save old value, users cannot change type
1087 repo_type=repo_type,
1085 repo_type=repo_type,
1088
1086
1089 repo_name=updates['repo_name'],
1087 repo_name=updates['repo_name'],
1090 repo_owner=updates['user'],
1088 repo_owner=updates['user'],
1091 repo_description=updates['repo_description'],
1089 repo_description=updates['repo_description'],
1092 repo_clone_uri=updates['clone_uri'],
1090 repo_clone_uri=updates['clone_uri'],
1093 repo_push_uri=updates['push_uri'],
1091 repo_push_uri=updates['push_uri'],
1094 repo_fork_of=updates['fork_id'],
1092 repo_fork_of=updates['fork_id'],
1095 repo_private=updates['repo_private'],
1093 repo_private=updates['repo_private'],
1096 repo_landing_commit_ref=updates['repo_landing_rev'],
1094 repo_landing_commit_ref=updates['repo_landing_rev'],
1097 repo_enable_statistics=updates['repo_enable_statistics'],
1095 repo_enable_statistics=updates['repo_enable_statistics'],
1098 repo_enable_downloads=updates['repo_enable_downloads'],
1096 repo_enable_downloads=updates['repo_enable_downloads'],
1099 repo_enable_locking=updates['repo_enable_locking']))
1097 repo_enable_locking=updates['repo_enable_locking']))
1100 except validation_schema.Invalid as err:
1098 except validation_schema.Invalid as err:
1101 raise JSONRPCValidationError(colander_exc=err)
1099 raise JSONRPCValidationError(colander_exc=err)
1102
1100
1103 # save validated data back into the updates dict
1101 # save validated data back into the updates dict
1104 validated_updates = dict(
1102 validated_updates = dict(
1105 repo_name=schema_data['repo_group']['repo_name_without_group'],
1103 repo_name=schema_data['repo_group']['repo_name_without_group'],
1106 repo_group=schema_data['repo_group']['repo_group_id'],
1104 repo_group=schema_data['repo_group']['repo_group_id'],
1107
1105
1108 user=schema_data['repo_owner'],
1106 user=schema_data['repo_owner'],
1109 repo_description=schema_data['repo_description'],
1107 repo_description=schema_data['repo_description'],
1110 repo_private=schema_data['repo_private'],
1108 repo_private=schema_data['repo_private'],
1111 clone_uri=schema_data['repo_clone_uri'],
1109 clone_uri=schema_data['repo_clone_uri'],
1112 push_uri=schema_data['repo_push_uri'],
1110 push_uri=schema_data['repo_push_uri'],
1113 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1111 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1114 repo_enable_statistics=schema_data['repo_enable_statistics'],
1112 repo_enable_statistics=schema_data['repo_enable_statistics'],
1115 repo_enable_locking=schema_data['repo_enable_locking'],
1113 repo_enable_locking=schema_data['repo_enable_locking'],
1116 repo_enable_downloads=schema_data['repo_enable_downloads'],
1114 repo_enable_downloads=schema_data['repo_enable_downloads'],
1117 )
1115 )
1118
1116
1119 if schema_data['repo_fork_of']:
1117 if schema_data['repo_fork_of']:
1120 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1118 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1121 validated_updates['fork_id'] = fork_repo.repo_id
1119 validated_updates['fork_id'] = fork_repo.repo_id
1122
1120
1123 # extra fields
1121 # extra fields
1124 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1122 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1125 if fields:
1123 if fields:
1126 validated_updates.update(fields)
1124 validated_updates.update(fields)
1127
1125
1128 try:
1126 try:
1129 RepoModel().update(repo, **validated_updates)
1127 RepoModel().update(repo, **validated_updates)
1130 audit_logger.store_api(
1128 audit_logger.store_api(
1131 'repo.edit', action_data={'old_data': old_values},
1129 'repo.edit', action_data={'old_data': old_values},
1132 user=apiuser, repo=repo)
1130 user=apiuser, repo=repo)
1133 Session().commit()
1131 Session().commit()
1134 return {
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 'repository': repo.get_api_data(include_secrets=include_secrets)
1134 'repository': repo.get_api_data(include_secrets=include_secrets)
1137 }
1135 }
1138 except Exception:
1136 except Exception:
1139 log.exception(
1137 log.exception(
1140 u"Exception while trying to update the repository %s",
1138 "Exception while trying to update the repository %s",
1141 repoid)
1139 repoid)
1142 raise JSONRPCError('failed to update repo `%s`' % repoid)
1140 raise JSONRPCError('failed to update repo `%s`' % repoid)
1143
1141
1144
1142
1145 @jsonrpc_method()
1143 @jsonrpc_method()
1146 def fork_repo(request, apiuser, repoid, fork_name,
1144 def fork_repo(request, apiuser, repoid, fork_name,
1147 owner=Optional(OAttr('apiuser')),
1145 owner=Optional(OAttr('apiuser')),
1148 description=Optional(''),
1146 description=Optional(''),
1149 private=Optional(False),
1147 private=Optional(False),
1150 clone_uri=Optional(None),
1148 clone_uri=Optional(None),
1151 landing_rev=Optional(None),
1149 landing_rev=Optional(None),
1152 copy_permissions=Optional(False)):
1150 copy_permissions=Optional(False)):
1153 """
1151 """
1154 Creates a fork of the specified |repo|.
1152 Creates a fork of the specified |repo|.
1155
1153
1156 * If the fork_name contains "/", fork will be created inside
1154 * If the fork_name contains "/", fork will be created inside
1157 a repository group or nested repository groups
1155 a repository group or nested repository groups
1158
1156
1159 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1157 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1160 inside group "foo/bar". You have to have permissions to access and
1158 inside group "foo/bar". You have to have permissions to access and
1161 write to the last repository group ("bar" in this example)
1159 write to the last repository group ("bar" in this example)
1162
1160
1163 This command can only be run using an |authtoken| with minimum
1161 This command can only be run using an |authtoken| with minimum
1164 read permissions of the forked repo, create fork permissions for an user.
1162 read permissions of the forked repo, create fork permissions for an user.
1165
1163
1166 :param apiuser: This is filled automatically from the |authtoken|.
1164 :param apiuser: This is filled automatically from the |authtoken|.
1167 :type apiuser: AuthUser
1165 :type apiuser: AuthUser
1168 :param repoid: Set repository name or repository ID.
1166 :param repoid: Set repository name or repository ID.
1169 :type repoid: str or int
1167 :type repoid: str or int
1170 :param fork_name: Set the fork name, including it's repository group membership.
1168 :param fork_name: Set the fork name, including it's repository group membership.
1171 :type fork_name: str
1169 :type fork_name: str
1172 :param owner: Set the fork owner.
1170 :param owner: Set the fork owner.
1173 :type owner: str
1171 :type owner: str
1174 :param description: Set the fork description.
1172 :param description: Set the fork description.
1175 :type description: str
1173 :type description: str
1176 :param copy_permissions: Copy permissions from parent |repo|. The
1174 :param copy_permissions: Copy permissions from parent |repo|. The
1177 default is False.
1175 default is False.
1178 :type copy_permissions: bool
1176 :type copy_permissions: bool
1179 :param private: Make the fork private. The default is False.
1177 :param private: Make the fork private. The default is False.
1180 :type private: bool
1178 :type private: bool
1181 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1179 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1182
1180
1183 Example output:
1181 Example output:
1184
1182
1185 .. code-block:: bash
1183 .. code-block:: bash
1186
1184
1187 id : <id_for_response>
1185 id : <id_for_response>
1188 api_key : "<api_key>"
1186 api_key : "<api_key>"
1189 args: {
1187 args: {
1190 "repoid" : "<reponame or repo_id>",
1188 "repoid" : "<reponame or repo_id>",
1191 "fork_name": "<forkname>",
1189 "fork_name": "<forkname>",
1192 "owner": "<username or user_id = Optional(=apiuser)>",
1190 "owner": "<username or user_id = Optional(=apiuser)>",
1193 "description": "<description>",
1191 "description": "<description>",
1194 "copy_permissions": "<bool>",
1192 "copy_permissions": "<bool>",
1195 "private": "<bool>",
1193 "private": "<bool>",
1196 "landing_rev": "<landing_rev>"
1194 "landing_rev": "<landing_rev>"
1197 }
1195 }
1198
1196
1199 Example error output:
1197 Example error output:
1200
1198
1201 .. code-block:: bash
1199 .. code-block:: bash
1202
1200
1203 id : <id_given_in_input>
1201 id : <id_given_in_input>
1204 result: {
1202 result: {
1205 "msg": "Created fork of `<reponame>` as `<forkname>`",
1203 "msg": "Created fork of `<reponame>` as `<forkname>`",
1206 "success": true,
1204 "success": true,
1207 "task": "<celery task id or None if done sync>"
1205 "task": "<celery task id or None if done sync>"
1208 }
1206 }
1209 error: null
1207 error: null
1210
1208
1211 """
1209 """
1212
1210
1213 repo = get_repo_or_error(repoid)
1211 repo = get_repo_or_error(repoid)
1214 repo_name = repo.repo_name
1212 repo_name = repo.repo_name
1215
1213
1216 if not has_superadmin_permission(apiuser):
1214 if not has_superadmin_permission(apiuser):
1217 # check if we have at least read permission for
1215 # check if we have at least read permission for
1218 # this repo that we fork !
1216 # this repo that we fork !
1219 _perms = ('repository.admin', 'repository.write', 'repository.read')
1217 _perms = ('repository.admin', 'repository.write', 'repository.read')
1220 validate_repo_permissions(apiuser, repoid, repo, _perms)
1218 validate_repo_permissions(apiuser, repoid, repo, _perms)
1221
1219
1222 # check if the regular user has at least fork permissions as well
1220 # check if the regular user has at least fork permissions as well
1223 if not HasPermissionAnyApi(PermissionModel.FORKING_ENABLED)(user=apiuser):
1221 if not HasPermissionAnyApi(PermissionModel.FORKING_ENABLED)(user=apiuser):
1224 raise JSONRPCForbidden()
1222 raise JSONRPCForbidden()
1225
1223
1226 # check if user can set owner parameter
1224 # check if user can set owner parameter
1227 owner = validate_set_owner_permissions(apiuser, owner)
1225 owner = validate_set_owner_permissions(apiuser, owner)
1228
1226
1229 description = Optional.extract(description)
1227 description = Optional.extract(description)
1230 copy_permissions = Optional.extract(copy_permissions)
1228 copy_permissions = Optional.extract(copy_permissions)
1231 clone_uri = Optional.extract(clone_uri)
1229 clone_uri = Optional.extract(clone_uri)
1232
1230
1233 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1231 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1234 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1232 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1235 ref_choices = list(set(ref_choices + [landing_ref]))
1233 ref_choices = list(set(ref_choices + [landing_ref]))
1236 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1234 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1237
1235
1238 private = Optional.extract(private)
1236 private = Optional.extract(private)
1239
1237
1240 schema = repo_schema.RepoSchema().bind(
1238 schema = repo_schema.RepoSchema().bind(
1241 repo_type_options=rhodecode.BACKENDS.keys(),
1239 repo_type_options=rhodecode.BACKENDS.keys(),
1242 repo_ref_options=ref_choices,
1240 repo_ref_options=ref_choices,
1243 repo_type=repo.repo_type,
1241 repo_type=repo.repo_type,
1244 # user caller
1242 # user caller
1245 user=apiuser)
1243 user=apiuser)
1246
1244
1247 try:
1245 try:
1248 schema_data = schema.deserialize(dict(
1246 schema_data = schema.deserialize(dict(
1249 repo_name=fork_name,
1247 repo_name=fork_name,
1250 repo_type=repo.repo_type,
1248 repo_type=repo.repo_type,
1251 repo_owner=owner.username,
1249 repo_owner=owner.username,
1252 repo_description=description,
1250 repo_description=description,
1253 repo_landing_commit_ref=landing_commit_ref,
1251 repo_landing_commit_ref=landing_commit_ref,
1254 repo_clone_uri=clone_uri,
1252 repo_clone_uri=clone_uri,
1255 repo_private=private,
1253 repo_private=private,
1256 repo_copy_permissions=copy_permissions))
1254 repo_copy_permissions=copy_permissions))
1257 except validation_schema.Invalid as err:
1255 except validation_schema.Invalid as err:
1258 raise JSONRPCValidationError(colander_exc=err)
1256 raise JSONRPCValidationError(colander_exc=err)
1259
1257
1260 try:
1258 try:
1261 data = {
1259 data = {
1262 'fork_parent_id': repo.repo_id,
1260 'fork_parent_id': repo.repo_id,
1263
1261
1264 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1262 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1265 'repo_name_full': schema_data['repo_name'],
1263 'repo_name_full': schema_data['repo_name'],
1266 'repo_group': schema_data['repo_group']['repo_group_id'],
1264 'repo_group': schema_data['repo_group']['repo_group_id'],
1267 'repo_type': schema_data['repo_type'],
1265 'repo_type': schema_data['repo_type'],
1268 'description': schema_data['repo_description'],
1266 'description': schema_data['repo_description'],
1269 'private': schema_data['repo_private'],
1267 'private': schema_data['repo_private'],
1270 'copy_permissions': schema_data['repo_copy_permissions'],
1268 'copy_permissions': schema_data['repo_copy_permissions'],
1271 'landing_rev': schema_data['repo_landing_commit_ref'],
1269 'landing_rev': schema_data['repo_landing_commit_ref'],
1272 }
1270 }
1273
1271
1274 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1272 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1275 # no commit, it's done in RepoModel, or async via celery
1273 # no commit, it's done in RepoModel, or async via celery
1276 task_id = get_task_id(task)
1274 task_id = get_task_id(task)
1277
1275
1278 return {
1276 return {
1279 'msg': 'Created fork of `%s` as `%s`' % (
1277 'msg': 'Created fork of `{}` as `{}`'.format(
1280 repo.repo_name, schema_data['repo_name']),
1278 repo.repo_name, schema_data['repo_name']),
1281 'success': True, # cannot return the repo data here since fork
1279 'success': True, # cannot return the repo data here since fork
1282 # can be done async
1280 # can be done async
1283 'task': task_id
1281 'task': task_id
1284 }
1282 }
1285 except Exception:
1283 except Exception:
1286 log.exception(
1284 log.exception(
1287 u"Exception while trying to create fork %s",
1285 "Exception while trying to create fork %s",
1288 schema_data['repo_name'])
1286 schema_data['repo_name'])
1289 raise JSONRPCError(
1287 raise JSONRPCError(
1290 'failed to fork repository `%s` as `%s`' % (
1288 'failed to fork repository `{}` as `{}`'.format(
1291 repo_name, schema_data['repo_name']))
1289 repo_name, schema_data['repo_name']))
1292
1290
1293
1291
1294 @jsonrpc_method()
1292 @jsonrpc_method()
1295 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1293 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1296 """
1294 """
1297 Deletes a repository.
1295 Deletes a repository.
1298
1296
1299 * When the `forks` parameter is set it's possible to detach or delete
1297 * When the `forks` parameter is set it's possible to detach or delete
1300 forks of deleted repository.
1298 forks of deleted repository.
1301
1299
1302 This command can only be run using an |authtoken| with admin
1300 This command can only be run using an |authtoken| with admin
1303 permissions on the |repo|.
1301 permissions on the |repo|.
1304
1302
1305 :param apiuser: This is filled automatically from the |authtoken|.
1303 :param apiuser: This is filled automatically from the |authtoken|.
1306 :type apiuser: AuthUser
1304 :type apiuser: AuthUser
1307 :param repoid: Set the repository name or repository ID.
1305 :param repoid: Set the repository name or repository ID.
1308 :type repoid: str or int
1306 :type repoid: str or int
1309 :param forks: Set to `detach` or `delete` forks from the |repo|.
1307 :param forks: Set to `detach` or `delete` forks from the |repo|.
1310 :type forks: Optional(str)
1308 :type forks: Optional(str)
1311
1309
1312 Example error output:
1310 Example error output:
1313
1311
1314 .. code-block:: bash
1312 .. code-block:: bash
1315
1313
1316 id : <id_given_in_input>
1314 id : <id_given_in_input>
1317 result: {
1315 result: {
1318 "msg": "Deleted repository `<reponame>`",
1316 "msg": "Deleted repository `<reponame>`",
1319 "success": true
1317 "success": true
1320 }
1318 }
1321 error: null
1319 error: null
1322 """
1320 """
1323
1321
1324 repo = get_repo_or_error(repoid)
1322 repo = get_repo_or_error(repoid)
1325 repo_name = repo.repo_name
1323 repo_name = repo.repo_name
1326 if not has_superadmin_permission(apiuser):
1324 if not has_superadmin_permission(apiuser):
1327 _perms = ('repository.admin',)
1325 _perms = ('repository.admin',)
1328 validate_repo_permissions(apiuser, repoid, repo, _perms)
1326 validate_repo_permissions(apiuser, repoid, repo, _perms)
1329
1327
1330 try:
1328 try:
1331 handle_forks = Optional.extract(forks)
1329 handle_forks = Optional.extract(forks)
1332 _forks_msg = ''
1330 _forks_msg = ''
1333 _forks = [f for f in repo.forks]
1331 _forks = [f for f in repo.forks]
1334 if handle_forks == 'detach':
1332 if handle_forks == 'detach':
1335 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1333 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1336 elif handle_forks == 'delete':
1334 elif handle_forks == 'delete':
1337 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1335 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1338 elif _forks:
1336 elif _forks:
1339 raise JSONRPCError(
1337 raise JSONRPCError(
1340 'Cannot delete `%s` it still contains attached forks' %
1338 'Cannot delete `%s` it still contains attached forks' %
1341 (repo.repo_name,)
1339 (repo.repo_name,)
1342 )
1340 )
1343 old_data = repo.get_api_data()
1341 old_data = repo.get_api_data()
1344 RepoModel().delete(repo, forks=forks)
1342 RepoModel().delete(repo, forks=forks)
1345
1343
1346 repo = audit_logger.RepoWrap(repo_id=None,
1344 repo = audit_logger.RepoWrap(repo_id=None,
1347 repo_name=repo.repo_name)
1345 repo_name=repo.repo_name)
1348
1346
1349 audit_logger.store_api(
1347 audit_logger.store_api(
1350 'repo.delete', action_data={'old_data': old_data},
1348 'repo.delete', action_data={'old_data': old_data},
1351 user=apiuser, repo=repo)
1349 user=apiuser, repo=repo)
1352
1350
1353 ScmModel().mark_for_invalidation(repo_name, delete=True)
1351 ScmModel().mark_for_invalidation(repo_name, delete=True)
1354 Session().commit()
1352 Session().commit()
1355 return {
1353 return {
1356 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1354 'msg': 'Deleted repository `{}`{}'.format(repo_name, _forks_msg),
1357 'success': True
1355 'success': True
1358 }
1356 }
1359 except Exception:
1357 except Exception:
1360 log.exception("Exception occurred while trying to delete repo")
1358 log.exception("Exception occurred while trying to delete repo")
1361 raise JSONRPCError(
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 #TODO: marcink, change name ?
1364 #TODO: marcink, change name ?
1367 @jsonrpc_method()
1365 @jsonrpc_method()
1368 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1366 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1369 """
1367 """
1370 Invalidates the cache for the specified repository.
1368 Invalidates the cache for the specified repository.
1371
1369
1372 This command can only be run using an |authtoken| with admin rights to
1370 This command can only be run using an |authtoken| with admin rights to
1373 the specified repository.
1371 the specified repository.
1374
1372
1375 This command takes the following options:
1373 This command takes the following options:
1376
1374
1377 :param apiuser: This is filled automatically from |authtoken|.
1375 :param apiuser: This is filled automatically from |authtoken|.
1378 :type apiuser: AuthUser
1376 :type apiuser: AuthUser
1379 :param repoid: Sets the repository name or repository ID.
1377 :param repoid: Sets the repository name or repository ID.
1380 :type repoid: str or int
1378 :type repoid: str or int
1381 :param delete_keys: This deletes the invalidated keys instead of
1379 :param delete_keys: This deletes the invalidated keys instead of
1382 just flagging them.
1380 just flagging them.
1383 :type delete_keys: Optional(``True`` | ``False``)
1381 :type delete_keys: Optional(``True`` | ``False``)
1384
1382
1385 Example output:
1383 Example output:
1386
1384
1387 .. code-block:: bash
1385 .. code-block:: bash
1388
1386
1389 id : <id_given_in_input>
1387 id : <id_given_in_input>
1390 result : {
1388 result : {
1391 'msg': Cache for repository `<repository name>` was invalidated,
1389 'msg': Cache for repository `<repository name>` was invalidated,
1392 'repository': <repository name>
1390 'repository': <repository name>
1393 }
1391 }
1394 error : null
1392 error : null
1395
1393
1396 Example error output:
1394 Example error output:
1397
1395
1398 .. code-block:: bash
1396 .. code-block:: bash
1399
1397
1400 id : <id_given_in_input>
1398 id : <id_given_in_input>
1401 result : null
1399 result : null
1402 error : {
1400 error : {
1403 'Error occurred during cache invalidation action'
1401 'Error occurred during cache invalidation action'
1404 }
1402 }
1405
1403
1406 """
1404 """
1407
1405
1408 repo = get_repo_or_error(repoid)
1406 repo = get_repo_or_error(repoid)
1409 if not has_superadmin_permission(apiuser):
1407 if not has_superadmin_permission(apiuser):
1410 _perms = ('repository.admin', 'repository.write',)
1408 _perms = ('repository.admin', 'repository.write',)
1411 validate_repo_permissions(apiuser, repoid, repo, _perms)
1409 validate_repo_permissions(apiuser, repoid, repo, _perms)
1412
1410
1413 delete = Optional.extract(delete_keys)
1411 delete = Optional.extract(delete_keys)
1414 try:
1412 try:
1415 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1413 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1416 return {
1414 return {
1417 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1415 'msg': 'Cache for repository `{}` was invalidated'.format(repoid),
1418 'repository': repo.repo_name
1416 'repository': repo.repo_name
1419 }
1417 }
1420 except Exception:
1418 except Exception:
1421 log.exception(
1419 log.exception(
1422 "Exception occurred while trying to invalidate repo cache")
1420 "Exception occurred while trying to invalidate repo cache")
1423 raise JSONRPCError(
1421 raise JSONRPCError(
1424 'Error occurred during cache invalidation action'
1422 'Error occurred during cache invalidation action'
1425 )
1423 )
1426
1424
1427
1425
1428 #TODO: marcink, change name ?
1426 #TODO: marcink, change name ?
1429 @jsonrpc_method()
1427 @jsonrpc_method()
1430 def lock(request, apiuser, repoid, locked=Optional(None),
1428 def lock(request, apiuser, repoid, locked=Optional(None),
1431 userid=Optional(OAttr('apiuser'))):
1429 userid=Optional(OAttr('apiuser'))):
1432 """
1430 """
1433 Sets the lock state of the specified |repo| by the given user.
1431 Sets the lock state of the specified |repo| by the given user.
1434 From more information, see :ref:`repo-locking`.
1432 From more information, see :ref:`repo-locking`.
1435
1433
1436 * If the ``userid`` option is not set, the repository is locked to the
1434 * If the ``userid`` option is not set, the repository is locked to the
1437 user who called the method.
1435 user who called the method.
1438 * If the ``locked`` parameter is not set, the current lock state of the
1436 * If the ``locked`` parameter is not set, the current lock state of the
1439 repository is displayed.
1437 repository is displayed.
1440
1438
1441 This command can only be run using an |authtoken| with admin rights to
1439 This command can only be run using an |authtoken| with admin rights to
1442 the specified repository.
1440 the specified repository.
1443
1441
1444 This command takes the following options:
1442 This command takes the following options:
1445
1443
1446 :param apiuser: This is filled automatically from the |authtoken|.
1444 :param apiuser: This is filled automatically from the |authtoken|.
1447 :type apiuser: AuthUser
1445 :type apiuser: AuthUser
1448 :param repoid: Sets the repository name or repository ID.
1446 :param repoid: Sets the repository name or repository ID.
1449 :type repoid: str or int
1447 :type repoid: str or int
1450 :param locked: Sets the lock state.
1448 :param locked: Sets the lock state.
1451 :type locked: Optional(``True`` | ``False``)
1449 :type locked: Optional(``True`` | ``False``)
1452 :param userid: Set the repository lock to this user.
1450 :param userid: Set the repository lock to this user.
1453 :type userid: Optional(str or int)
1451 :type userid: Optional(str or int)
1454
1452
1455 Example error output:
1453 Example error output:
1456
1454
1457 .. code-block:: bash
1455 .. code-block:: bash
1458
1456
1459 id : <id_given_in_input>
1457 id : <id_given_in_input>
1460 result : {
1458 result : {
1461 'repo': '<reponame>',
1459 'repo': '<reponame>',
1462 'locked': <bool: lock state>,
1460 'locked': <bool: lock state>,
1463 'locked_since': <int: lock timestamp>,
1461 'locked_since': <int: lock timestamp>,
1464 'locked_by': <username of person who made the lock>,
1462 'locked_by': <username of person who made the lock>,
1465 'lock_reason': <str: reason for locking>,
1463 'lock_reason': <str: reason for locking>,
1466 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1464 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1467 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1465 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1468 or
1466 or
1469 'msg': 'Repo `<repository name>` not locked.'
1467 'msg': 'Repo `<repository name>` not locked.'
1470 or
1468 or
1471 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1469 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1472 }
1470 }
1473 error : null
1471 error : null
1474
1472
1475 Example error output:
1473 Example error output:
1476
1474
1477 .. code-block:: bash
1475 .. code-block:: bash
1478
1476
1479 id : <id_given_in_input>
1477 id : <id_given_in_input>
1480 result : null
1478 result : null
1481 error : {
1479 error : {
1482 'Error occurred locking repository `<reponame>`'
1480 'Error occurred locking repository `<reponame>`'
1483 }
1481 }
1484 """
1482 """
1485
1483
1486 repo = get_repo_or_error(repoid)
1484 repo = get_repo_or_error(repoid)
1487 if not has_superadmin_permission(apiuser):
1485 if not has_superadmin_permission(apiuser):
1488 # check if we have at least write permission for this repo !
1486 # check if we have at least write permission for this repo !
1489 _perms = ('repository.admin', 'repository.write',)
1487 _perms = ('repository.admin', 'repository.write',)
1490 validate_repo_permissions(apiuser, repoid, repo, _perms)
1488 validate_repo_permissions(apiuser, repoid, repo, _perms)
1491
1489
1492 # make sure normal user does not pass someone else userid,
1490 # make sure normal user does not pass someone else userid,
1493 # he is not allowed to do that
1491 # he is not allowed to do that
1494 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1492 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1495 raise JSONRPCError('userid is not the same as your user')
1493 raise JSONRPCError('userid is not the same as your user')
1496
1494
1497 if isinstance(userid, Optional):
1495 if isinstance(userid, Optional):
1498 userid = apiuser.user_id
1496 userid = apiuser.user_id
1499
1497
1500 user = get_user_or_error(userid)
1498 user = get_user_or_error(userid)
1501
1499
1502 if isinstance(locked, Optional):
1500 if isinstance(locked, Optional):
1503 lockobj = repo.locked
1501 lockobj = repo.locked
1504
1502
1505 if lockobj[0] is None:
1503 if lockobj[0] is None:
1506 _d = {
1504 _d = {
1507 'repo': repo.repo_name,
1505 'repo': repo.repo_name,
1508 'locked': False,
1506 'locked': False,
1509 'locked_since': None,
1507 'locked_since': None,
1510 'locked_by': None,
1508 'locked_by': None,
1511 'lock_reason': None,
1509 'lock_reason': None,
1512 'lock_state_changed': False,
1510 'lock_state_changed': False,
1513 'msg': 'Repo `%s` not locked.' % repo.repo_name
1511 'msg': 'Repo `%s` not locked.' % repo.repo_name
1514 }
1512 }
1515 return _d
1513 return _d
1516 else:
1514 else:
1517 _user_id, _time, _reason = lockobj
1515 _user_id, _time, _reason = lockobj
1518 lock_user = get_user_or_error(userid)
1516 lock_user = get_user_or_error(userid)
1519 _d = {
1517 _d = {
1520 'repo': repo.repo_name,
1518 'repo': repo.repo_name,
1521 'locked': True,
1519 'locked': True,
1522 'locked_since': _time,
1520 'locked_since': _time,
1523 'locked_by': lock_user.username,
1521 'locked_by': lock_user.username,
1524 'lock_reason': _reason,
1522 'lock_reason': _reason,
1525 'lock_state_changed': False,
1523 'lock_state_changed': False,
1526 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1524 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1527 % (repo.repo_name, lock_user.username,
1525 % (repo.repo_name, lock_user.username,
1528 json.dumps(time_to_datetime(_time))))
1526 json.dumps(time_to_datetime(_time))))
1529 }
1527 }
1530 return _d
1528 return _d
1531
1529
1532 # force locked state through a flag
1530 # force locked state through a flag
1533 else:
1531 else:
1534 locked = str2bool(locked)
1532 locked = str2bool(locked)
1535 lock_reason = Repository.LOCK_API
1533 lock_reason = Repository.LOCK_API
1536 try:
1534 try:
1537 if locked:
1535 if locked:
1538 lock_time = time.time()
1536 lock_time = time.time()
1539 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1537 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1540 else:
1538 else:
1541 lock_time = None
1539 lock_time = None
1542 Repository.unlock(repo)
1540 Repository.unlock(repo)
1543 _d = {
1541 _d = {
1544 'repo': repo.repo_name,
1542 'repo': repo.repo_name,
1545 'locked': locked,
1543 'locked': locked,
1546 'locked_since': lock_time,
1544 'locked_since': lock_time,
1547 'locked_by': user.username,
1545 'locked_by': user.username,
1548 'lock_reason': lock_reason,
1546 'lock_reason': lock_reason,
1549 'lock_state_changed': True,
1547 'lock_state_changed': True,
1550 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1548 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1551 % (user.username, repo.repo_name, locked))
1549 % (user.username, repo.repo_name, locked))
1552 }
1550 }
1553 return _d
1551 return _d
1554 except Exception:
1552 except Exception:
1555 log.exception(
1553 log.exception(
1556 "Exception occurred while trying to lock repository")
1554 "Exception occurred while trying to lock repository")
1557 raise JSONRPCError(
1555 raise JSONRPCError(
1558 'Error occurred locking repository `%s`' % repo.repo_name
1556 'Error occurred locking repository `%s`' % repo.repo_name
1559 )
1557 )
1560
1558
1561
1559
1562 @jsonrpc_method()
1560 @jsonrpc_method()
1563 def comment_commit(
1561 def comment_commit(
1564 request, apiuser, repoid, commit_id, message, status=Optional(None),
1562 request, apiuser, repoid, commit_id, message, status=Optional(None),
1565 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1563 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1566 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1564 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1567 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1565 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1568 """
1566 """
1569 Set a commit comment, and optionally change the status of the commit.
1567 Set a commit comment, and optionally change the status of the commit.
1570
1568
1571 :param apiuser: This is filled automatically from the |authtoken|.
1569 :param apiuser: This is filled automatically from the |authtoken|.
1572 :type apiuser: AuthUser
1570 :type apiuser: AuthUser
1573 :param repoid: Set the repository name or repository ID.
1571 :param repoid: Set the repository name or repository ID.
1574 :type repoid: str or int
1572 :type repoid: str or int
1575 :param commit_id: Specify the commit_id for which to set a comment.
1573 :param commit_id: Specify the commit_id for which to set a comment.
1576 :type commit_id: str
1574 :type commit_id: str
1577 :param message: The comment text.
1575 :param message: The comment text.
1578 :type message: str
1576 :type message: str
1579 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1577 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1580 'approved', 'rejected', 'under_review'
1578 'approved', 'rejected', 'under_review'
1581 :type status: str
1579 :type status: str
1582 :param comment_type: Comment type, one of: 'note', 'todo'
1580 :param comment_type: Comment type, one of: 'note', 'todo'
1583 :type comment_type: Optional(str), default: 'note'
1581 :type comment_type: Optional(str), default: 'note'
1584 :param resolves_comment_id: id of comment which this one will resolve
1582 :param resolves_comment_id: id of comment which this one will resolve
1585 :type resolves_comment_id: Optional(int)
1583 :type resolves_comment_id: Optional(int)
1586 :param extra_recipients: list of user ids or usernames to add
1584 :param extra_recipients: list of user ids or usernames to add
1587 notifications for this comment. Acts like a CC for notification
1585 notifications for this comment. Acts like a CC for notification
1588 :type extra_recipients: Optional(list)
1586 :type extra_recipients: Optional(list)
1589 :param userid: Set the user name of the comment creator.
1587 :param userid: Set the user name of the comment creator.
1590 :type userid: Optional(str or int)
1588 :type userid: Optional(str or int)
1591 :param send_email: Define if this comment should also send email notification
1589 :param send_email: Define if this comment should also send email notification
1592 :type send_email: Optional(bool)
1590 :type send_email: Optional(bool)
1593
1591
1594 Example error output:
1592 Example error output:
1595
1593
1596 .. code-block:: bash
1594 .. code-block:: bash
1597
1595
1598 {
1596 {
1599 "id" : <id_given_in_input>,
1597 "id" : <id_given_in_input>,
1600 "result" : {
1598 "result" : {
1601 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1599 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1602 "status_change": null or <status>,
1600 "status_change": null or <status>,
1603 "success": true
1601 "success": true
1604 },
1602 },
1605 "error" : null
1603 "error" : null
1606 }
1604 }
1607
1605
1608 """
1606 """
1609 _ = request.translate
1607 _ = request.translate
1610
1608
1611 repo = get_repo_or_error(repoid)
1609 repo = get_repo_or_error(repoid)
1612 if not has_superadmin_permission(apiuser):
1610 if not has_superadmin_permission(apiuser):
1613 _perms = ('repository.read', 'repository.write', 'repository.admin')
1611 _perms = ('repository.read', 'repository.write', 'repository.admin')
1614 validate_repo_permissions(apiuser, repoid, repo, _perms)
1612 validate_repo_permissions(apiuser, repoid, repo, _perms)
1615 db_repo_name = repo.repo_name
1613 db_repo_name = repo.repo_name
1616
1614
1617 try:
1615 try:
1618 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1616 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1619 commit_id = commit.raw_id
1617 commit_id = commit.raw_id
1620 except Exception as e:
1618 except Exception as e:
1621 log.exception('Failed to fetch commit')
1619 log.exception('Failed to fetch commit')
1622 raise JSONRPCError(safe_str(e))
1620 raise JSONRPCError(safe_str(e))
1623
1621
1624 if isinstance(userid, Optional):
1622 if isinstance(userid, Optional):
1625 userid = apiuser.user_id
1623 userid = apiuser.user_id
1626
1624
1627 user = get_user_or_error(userid)
1625 user = get_user_or_error(userid)
1628 status = Optional.extract(status)
1626 status = Optional.extract(status)
1629 comment_type = Optional.extract(comment_type)
1627 comment_type = Optional.extract(comment_type)
1630 resolves_comment_id = Optional.extract(resolves_comment_id)
1628 resolves_comment_id = Optional.extract(resolves_comment_id)
1631 extra_recipients = Optional.extract(extra_recipients)
1629 extra_recipients = Optional.extract(extra_recipients)
1632 send_email = Optional.extract(send_email, binary=True)
1630 send_email = Optional.extract(send_email, binary=True)
1633
1631
1634 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1632 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1635 if status and status not in allowed_statuses:
1633 if status and status not in allowed_statuses:
1636 raise JSONRPCError('Bad status, must be on '
1634 raise JSONRPCError('Bad status, must be on '
1637 'of %s got %s' % (allowed_statuses, status,))
1635 'of %s got %s' % (allowed_statuses, status,))
1638
1636
1639 if resolves_comment_id:
1637 if resolves_comment_id:
1640 comment = ChangesetComment.get(resolves_comment_id)
1638 comment = ChangesetComment.get(resolves_comment_id)
1641 if not comment:
1639 if not comment:
1642 raise JSONRPCError(
1640 raise JSONRPCError(
1643 'Invalid resolves_comment_id `%s` for this commit.'
1641 'Invalid resolves_comment_id `%s` for this commit.'
1644 % resolves_comment_id)
1642 % resolves_comment_id)
1645 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1643 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1646 raise JSONRPCError(
1644 raise JSONRPCError(
1647 'Comment `%s` is wrong type for setting status to resolved.'
1645 'Comment `%s` is wrong type for setting status to resolved.'
1648 % resolves_comment_id)
1646 % resolves_comment_id)
1649
1647
1650 try:
1648 try:
1651 rc_config = SettingsModel().get_all_settings()
1649 rc_config = SettingsModel().get_all_settings()
1652 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1650 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1653 status_change_label = ChangesetStatus.get_status_lbl(status)
1651 status_change_label = ChangesetStatus.get_status_lbl(status)
1654 comment = CommentsModel().create(
1652 comment = CommentsModel().create(
1655 message, repo, user, commit_id=commit_id,
1653 message, repo, user, commit_id=commit_id,
1656 status_change=status_change_label,
1654 status_change=status_change_label,
1657 status_change_type=status,
1655 status_change_type=status,
1658 renderer=renderer,
1656 renderer=renderer,
1659 comment_type=comment_type,
1657 comment_type=comment_type,
1660 resolves_comment_id=resolves_comment_id,
1658 resolves_comment_id=resolves_comment_id,
1661 auth_user=apiuser,
1659 auth_user=apiuser,
1662 extra_recipients=extra_recipients,
1660 extra_recipients=extra_recipients,
1663 send_email=send_email
1661 send_email=send_email
1664 )
1662 )
1665 is_inline = comment.is_inline
1663 is_inline = comment.is_inline
1666
1664
1667 if status:
1665 if status:
1668 # also do a status change
1666 # also do a status change
1669 try:
1667 try:
1670 ChangesetStatusModel().set_status(
1668 ChangesetStatusModel().set_status(
1671 repo, status, user, comment, revision=commit_id,
1669 repo, status, user, comment, revision=commit_id,
1672 dont_allow_on_closed_pull_request=True
1670 dont_allow_on_closed_pull_request=True
1673 )
1671 )
1674 except StatusChangeOnClosedPullRequestError:
1672 except StatusChangeOnClosedPullRequestError:
1675 log.exception(
1673 log.exception(
1676 "Exception occurred while trying to change repo commit status")
1674 "Exception occurred while trying to change repo commit status")
1677 msg = ('Changing status on a commit associated with '
1675 msg = ('Changing status on a commit associated with '
1678 'a closed pull request is not allowed')
1676 'a closed pull request is not allowed')
1679 raise JSONRPCError(msg)
1677 raise JSONRPCError(msg)
1680
1678
1681 CommentsModel().trigger_commit_comment_hook(
1679 CommentsModel().trigger_commit_comment_hook(
1682 repo, apiuser, 'create',
1680 repo, apiuser, 'create',
1683 data={'comment': comment, 'commit': commit})
1681 data={'comment': comment, 'commit': commit})
1684
1682
1685 Session().commit()
1683 Session().commit()
1686
1684
1687 comment_broadcast_channel = channelstream.comment_channel(
1685 comment_broadcast_channel = channelstream.comment_channel(
1688 db_repo_name, commit_obj=commit)
1686 db_repo_name, commit_obj=commit)
1689
1687
1690 comment_data = {'comment': comment, 'comment_id': comment.comment_id}
1688 comment_data = {'comment': comment, 'comment_id': comment.comment_id}
1691 comment_type = 'inline' if is_inline else 'general'
1689 comment_type = 'inline' if is_inline else 'general'
1692 channelstream.comment_channelstream_push(
1690 channelstream.comment_channelstream_push(
1693 request, comment_broadcast_channel, apiuser,
1691 request, comment_broadcast_channel, apiuser,
1694 _('posted a new {} comment').format(comment_type),
1692 _('posted a new {} comment').format(comment_type),
1695 comment_data=comment_data)
1693 comment_data=comment_data)
1696
1694
1697 return {
1695 return {
1698 'msg': (
1696 'msg': (
1699 'Commented on commit `%s` for repository `%s`' % (
1697 'Commented on commit `{}` for repository `{}`'.format(
1700 comment.revision, repo.repo_name)),
1698 comment.revision, repo.repo_name)),
1701 'status_change': status,
1699 'status_change': status,
1702 'success': True,
1700 'success': True,
1703 }
1701 }
1704 except JSONRPCError:
1702 except JSONRPCError:
1705 # catch any inside errors, and re-raise them to prevent from
1703 # catch any inside errors, and re-raise them to prevent from
1706 # below global catch to silence them
1704 # below global catch to silence them
1707 raise
1705 raise
1708 except Exception:
1706 except Exception:
1709 log.exception("Exception occurred while trying to comment on commit")
1707 log.exception("Exception occurred while trying to comment on commit")
1710 raise JSONRPCError(
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 @jsonrpc_method()
1713 @jsonrpc_method()
1716 def get_repo_comments(request, apiuser, repoid,
1714 def get_repo_comments(request, apiuser, repoid,
1717 commit_id=Optional(None), comment_type=Optional(None),
1715 commit_id=Optional(None), comment_type=Optional(None),
1718 userid=Optional(None)):
1716 userid=Optional(None)):
1719 """
1717 """
1720 Get all comments for a repository
1718 Get all comments for a repository
1721
1719
1722 :param apiuser: This is filled automatically from the |authtoken|.
1720 :param apiuser: This is filled automatically from the |authtoken|.
1723 :type apiuser: AuthUser
1721 :type apiuser: AuthUser
1724 :param repoid: Set the repository name or repository ID.
1722 :param repoid: Set the repository name or repository ID.
1725 :type repoid: str or int
1723 :type repoid: str or int
1726 :param commit_id: Optionally filter the comments by the commit_id
1724 :param commit_id: Optionally filter the comments by the commit_id
1727 :type commit_id: Optional(str), default: None
1725 :type commit_id: Optional(str), default: None
1728 :param comment_type: Optionally filter the comments by the comment_type
1726 :param comment_type: Optionally filter the comments by the comment_type
1729 one of: 'note', 'todo'
1727 one of: 'note', 'todo'
1730 :type comment_type: Optional(str), default: None
1728 :type comment_type: Optional(str), default: None
1731 :param userid: Optionally filter the comments by the author of comment
1729 :param userid: Optionally filter the comments by the author of comment
1732 :type userid: Optional(str or int), Default: None
1730 :type userid: Optional(str or int), Default: None
1733
1731
1734 Example error output:
1732 Example error output:
1735
1733
1736 .. code-block:: bash
1734 .. code-block:: bash
1737
1735
1738 {
1736 {
1739 "id" : <id_given_in_input>,
1737 "id" : <id_given_in_input>,
1740 "result" : [
1738 "result" : [
1741 {
1739 {
1742 "comment_author": <USER_DETAILS>,
1740 "comment_author": <USER_DETAILS>,
1743 "comment_created_on": "2017-02-01T14:38:16.309",
1741 "comment_created_on": "2017-02-01T14:38:16.309",
1744 "comment_f_path": "file.txt",
1742 "comment_f_path": "file.txt",
1745 "comment_id": 282,
1743 "comment_id": 282,
1746 "comment_lineno": "n1",
1744 "comment_lineno": "n1",
1747 "comment_resolved_by": null,
1745 "comment_resolved_by": null,
1748 "comment_status": [],
1746 "comment_status": [],
1749 "comment_text": "This file needs a header",
1747 "comment_text": "This file needs a header",
1750 "comment_type": "todo",
1748 "comment_type": "todo",
1751 "comment_last_version: 0
1749 "comment_last_version: 0
1752 }
1750 }
1753 ],
1751 ],
1754 "error" : null
1752 "error" : null
1755 }
1753 }
1756
1754
1757 """
1755 """
1758 repo = get_repo_or_error(repoid)
1756 repo = get_repo_or_error(repoid)
1759 if not has_superadmin_permission(apiuser):
1757 if not has_superadmin_permission(apiuser):
1760 _perms = ('repository.read', 'repository.write', 'repository.admin')
1758 _perms = ('repository.read', 'repository.write', 'repository.admin')
1761 validate_repo_permissions(apiuser, repoid, repo, _perms)
1759 validate_repo_permissions(apiuser, repoid, repo, _perms)
1762
1760
1763 commit_id = Optional.extract(commit_id)
1761 commit_id = Optional.extract(commit_id)
1764
1762
1765 userid = Optional.extract(userid)
1763 userid = Optional.extract(userid)
1766 if userid:
1764 if userid:
1767 user = get_user_or_error(userid)
1765 user = get_user_or_error(userid)
1768 else:
1766 else:
1769 user = None
1767 user = None
1770
1768
1771 comment_type = Optional.extract(comment_type)
1769 comment_type = Optional.extract(comment_type)
1772 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1770 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1773 raise JSONRPCError(
1771 raise JSONRPCError(
1774 'comment_type must be one of `{}` got {}'.format(
1772 'comment_type must be one of `{}` got {}'.format(
1775 ChangesetComment.COMMENT_TYPES, comment_type)
1773 ChangesetComment.COMMENT_TYPES, comment_type)
1776 )
1774 )
1777
1775
1778 comments = CommentsModel().get_repository_comments(
1776 comments = CommentsModel().get_repository_comments(
1779 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1777 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1780 return comments
1778 return comments
1781
1779
1782
1780
1783 @jsonrpc_method()
1781 @jsonrpc_method()
1784 def get_comment(request, apiuser, comment_id):
1782 def get_comment(request, apiuser, comment_id):
1785 """
1783 """
1786 Get single comment from repository or pull_request
1784 Get single comment from repository or pull_request
1787
1785
1788 :param apiuser: This is filled automatically from the |authtoken|.
1786 :param apiuser: This is filled automatically from the |authtoken|.
1789 :type apiuser: AuthUser
1787 :type apiuser: AuthUser
1790 :param comment_id: comment id found in the URL of comment
1788 :param comment_id: comment id found in the URL of comment
1791 :type comment_id: str or int
1789 :type comment_id: str or int
1792
1790
1793 Example error output:
1791 Example error output:
1794
1792
1795 .. code-block:: bash
1793 .. code-block:: bash
1796
1794
1797 {
1795 {
1798 "id" : <id_given_in_input>,
1796 "id" : <id_given_in_input>,
1799 "result" : {
1797 "result" : {
1800 "comment_author": <USER_DETAILS>,
1798 "comment_author": <USER_DETAILS>,
1801 "comment_created_on": "2017-02-01T14:38:16.309",
1799 "comment_created_on": "2017-02-01T14:38:16.309",
1802 "comment_f_path": "file.txt",
1800 "comment_f_path": "file.txt",
1803 "comment_id": 282,
1801 "comment_id": 282,
1804 "comment_lineno": "n1",
1802 "comment_lineno": "n1",
1805 "comment_resolved_by": null,
1803 "comment_resolved_by": null,
1806 "comment_status": [],
1804 "comment_status": [],
1807 "comment_text": "This file needs a header",
1805 "comment_text": "This file needs a header",
1808 "comment_type": "todo",
1806 "comment_type": "todo",
1809 "comment_last_version: 0
1807 "comment_last_version: 0
1810 },
1808 },
1811 "error" : null
1809 "error" : null
1812 }
1810 }
1813
1811
1814 """
1812 """
1815
1813
1816 comment = ChangesetComment.get(comment_id)
1814 comment = ChangesetComment.get(comment_id)
1817 if not comment:
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 perms = ('repository.read', 'repository.write', 'repository.admin')
1818 perms = ('repository.read', 'repository.write', 'repository.admin')
1821 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1819 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1822 (user=apiuser, repo_name=comment.repo.repo_name)
1820 (user=apiuser, repo_name=comment.repo.repo_name)
1823
1821
1824 if not has_comment_perm:
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 return comment
1825 return comment
1828
1826
1829
1827
1830 @jsonrpc_method()
1828 @jsonrpc_method()
1831 def edit_comment(request, apiuser, message, comment_id, version,
1829 def edit_comment(request, apiuser, message, comment_id, version,
1832 userid=Optional(OAttr('apiuser'))):
1830 userid=Optional(OAttr('apiuser'))):
1833 """
1831 """
1834 Edit comment on the pull request or commit,
1832 Edit comment on the pull request or commit,
1835 specified by the `comment_id` and version. Initially version should be 0
1833 specified by the `comment_id` and version. Initially version should be 0
1836
1834
1837 :param apiuser: This is filled automatically from the |authtoken|.
1835 :param apiuser: This is filled automatically from the |authtoken|.
1838 :type apiuser: AuthUser
1836 :type apiuser: AuthUser
1839 :param comment_id: Specify the comment_id for editing
1837 :param comment_id: Specify the comment_id for editing
1840 :type comment_id: int
1838 :type comment_id: int
1841 :param version: version of the comment that will be created, starts from 0
1839 :param version: version of the comment that will be created, starts from 0
1842 :type version: int
1840 :type version: int
1843 :param message: The text content of the comment.
1841 :param message: The text content of the comment.
1844 :type message: str
1842 :type message: str
1845 :param userid: Comment on the pull request as this user
1843 :param userid: Comment on the pull request as this user
1846 :type userid: Optional(str or int)
1844 :type userid: Optional(str or int)
1847
1845
1848 Example output:
1846 Example output:
1849
1847
1850 .. code-block:: bash
1848 .. code-block:: bash
1851
1849
1852 id : <id_given_in_input>
1850 id : <id_given_in_input>
1853 result : {
1851 result : {
1854 "comment": "<comment data>",
1852 "comment": "<comment data>",
1855 "version": "<Integer>",
1853 "version": "<Integer>",
1856 },
1854 },
1857 error : null
1855 error : null
1858 """
1856 """
1859
1857
1860 auth_user = apiuser
1858 auth_user = apiuser
1861 comment = ChangesetComment.get(comment_id)
1859 comment = ChangesetComment.get(comment_id)
1862 if not comment:
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 is_super_admin = has_superadmin_permission(apiuser)
1863 is_super_admin = has_superadmin_permission(apiuser)
1866 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1864 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1867 (user=apiuser, repo_name=comment.repo.repo_name)
1865 (user=apiuser, repo_name=comment.repo.repo_name)
1868
1866
1869 if not isinstance(userid, Optional):
1867 if not isinstance(userid, Optional):
1870 if is_super_admin or is_repo_admin:
1868 if is_super_admin or is_repo_admin:
1871 apiuser = get_user_or_error(userid)
1869 apiuser = get_user_or_error(userid)
1872 auth_user = apiuser.AuthUser()
1870 auth_user = apiuser.AuthUser()
1873 else:
1871 else:
1874 raise JSONRPCError('userid is not the same as your user')
1872 raise JSONRPCError('userid is not the same as your user')
1875
1873
1876 comment_author = comment.author.user_id == auth_user.user_id
1874 comment_author = comment.author.user_id == auth_user.user_id
1877
1875
1878 if comment.immutable:
1876 if comment.immutable:
1879 raise JSONRPCError("Immutable comment cannot be edited")
1877 raise JSONRPCError("Immutable comment cannot be edited")
1880
1878
1881 if not (is_super_admin or is_repo_admin or comment_author):
1879 if not (is_super_admin or is_repo_admin or comment_author):
1882 raise JSONRPCError("you don't have access to edit this comment")
1880 raise JSONRPCError("you don't have access to edit this comment")
1883
1881
1884 try:
1882 try:
1885 comment_history = CommentsModel().edit(
1883 comment_history = CommentsModel().edit(
1886 comment_id=comment_id,
1884 comment_id=comment_id,
1887 text=message,
1885 text=message,
1888 auth_user=auth_user,
1886 auth_user=auth_user,
1889 version=version,
1887 version=version,
1890 )
1888 )
1891 Session().commit()
1889 Session().commit()
1892 except CommentVersionMismatch:
1890 except CommentVersionMismatch:
1893 raise JSONRPCError(
1891 raise JSONRPCError(
1894 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1892 f'comment ({comment_id}) version ({version}) mismatch'
1895 )
1893 )
1896 if not comment_history and not message:
1894 if not comment_history and not message:
1897 raise JSONRPCError(
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 if comment.pull_request:
1899 if comment.pull_request:
1902 pull_request = comment.pull_request
1900 pull_request = comment.pull_request
1903 PullRequestModel().trigger_pull_request_hook(
1901 PullRequestModel().trigger_pull_request_hook(
1904 pull_request, apiuser, 'comment_edit',
1902 pull_request, apiuser, 'comment_edit',
1905 data={'comment': comment})
1903 data={'comment': comment})
1906 else:
1904 else:
1907 db_repo = comment.repo
1905 db_repo = comment.repo
1908 commit_id = comment.revision
1906 commit_id = comment.revision
1909 commit = db_repo.get_commit(commit_id)
1907 commit = db_repo.get_commit(commit_id)
1910 CommentsModel().trigger_commit_comment_hook(
1908 CommentsModel().trigger_commit_comment_hook(
1911 db_repo, apiuser, 'edit',
1909 db_repo, apiuser, 'edit',
1912 data={'comment': comment, 'commit': commit})
1910 data={'comment': comment, 'commit': commit})
1913
1911
1914 data = {
1912 data = {
1915 'comment': comment,
1913 'comment': comment,
1916 'version': comment_history.version if comment_history else None,
1914 'version': comment_history.version if comment_history else None,
1917 }
1915 }
1918 return data
1916 return data
1919
1917
1920
1918
1921 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1919 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1922 # @jsonrpc_method()
1920 # @jsonrpc_method()
1923 # def delete_comment(request, apiuser, comment_id):
1921 # def delete_comment(request, apiuser, comment_id):
1924 # auth_user = apiuser
1922 # auth_user = apiuser
1925 #
1923 #
1926 # comment = ChangesetComment.get(comment_id)
1924 # comment = ChangesetComment.get(comment_id)
1927 # if not comment:
1925 # if not comment:
1928 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1926 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1929 #
1927 #
1930 # is_super_admin = has_superadmin_permission(apiuser)
1928 # is_super_admin = has_superadmin_permission(apiuser)
1931 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1929 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1932 # (user=apiuser, repo_name=comment.repo.repo_name)
1930 # (user=apiuser, repo_name=comment.repo.repo_name)
1933 #
1931 #
1934 # comment_author = comment.author.user_id == auth_user.user_id
1932 # comment_author = comment.author.user_id == auth_user.user_id
1935 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1933 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1936 # raise JSONRPCError("you don't have access to edit this comment")
1934 # raise JSONRPCError("you don't have access to edit this comment")
1937
1935
1938 @jsonrpc_method()
1936 @jsonrpc_method()
1939 def grant_user_permission(request, apiuser, repoid, userid, perm):
1937 def grant_user_permission(request, apiuser, repoid, userid, perm):
1940 """
1938 """
1941 Grant permissions for the specified user on the given repository,
1939 Grant permissions for the specified user on the given repository,
1942 or update existing permissions if found.
1940 or update existing permissions if found.
1943
1941
1944 This command can only be run using an |authtoken| with admin
1942 This command can only be run using an |authtoken| with admin
1945 permissions on the |repo|.
1943 permissions on the |repo|.
1946
1944
1947 :param apiuser: This is filled automatically from the |authtoken|.
1945 :param apiuser: This is filled automatically from the |authtoken|.
1948 :type apiuser: AuthUser
1946 :type apiuser: AuthUser
1949 :param repoid: Set the repository name or repository ID.
1947 :param repoid: Set the repository name or repository ID.
1950 :type repoid: str or int
1948 :type repoid: str or int
1951 :param userid: Set the user name.
1949 :param userid: Set the user name.
1952 :type userid: str
1950 :type userid: str
1953 :param perm: Set the user permissions, using the following format
1951 :param perm: Set the user permissions, using the following format
1954 ``(repository.(none|read|write|admin))``
1952 ``(repository.(none|read|write|admin))``
1955 :type perm: str
1953 :type perm: str
1956
1954
1957 Example output:
1955 Example output:
1958
1956
1959 .. code-block:: bash
1957 .. code-block:: bash
1960
1958
1961 id : <id_given_in_input>
1959 id : <id_given_in_input>
1962 result: {
1960 result: {
1963 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1961 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1964 "success": true
1962 "success": true
1965 }
1963 }
1966 error: null
1964 error: null
1967 """
1965 """
1968
1966
1969 repo = get_repo_or_error(repoid)
1967 repo = get_repo_or_error(repoid)
1970 user = get_user_or_error(userid)
1968 user = get_user_or_error(userid)
1971 perm = get_perm_or_error(perm)
1969 perm = get_perm_or_error(perm)
1972 if not has_superadmin_permission(apiuser):
1970 if not has_superadmin_permission(apiuser):
1973 _perms = ('repository.admin',)
1971 _perms = ('repository.admin',)
1974 validate_repo_permissions(apiuser, repoid, repo, _perms)
1972 validate_repo_permissions(apiuser, repoid, repo, _perms)
1975
1973
1976 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1974 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1977 try:
1975 try:
1978 changes = RepoModel().update_permissions(
1976 changes = RepoModel().update_permissions(
1979 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1977 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1980
1978
1981 action_data = {
1979 action_data = {
1982 'added': changes['added'],
1980 'added': changes['added'],
1983 'updated': changes['updated'],
1981 'updated': changes['updated'],
1984 'deleted': changes['deleted'],
1982 'deleted': changes['deleted'],
1985 }
1983 }
1986 audit_logger.store_api(
1984 audit_logger.store_api(
1987 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1985 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1988 Session().commit()
1986 Session().commit()
1989 PermissionModel().flush_user_permission_caches(changes)
1987 PermissionModel().flush_user_permission_caches(changes)
1990
1988
1991 return {
1989 return {
1992 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1990 'msg': 'Granted perm: `{}` for user: `{}` in repo: `{}`'.format(
1993 perm.permission_name, user.username, repo.repo_name
1991 perm.permission_name, user.username, repo.repo_name
1994 ),
1992 ),
1995 'success': True
1993 'success': True
1996 }
1994 }
1997 except Exception:
1995 except Exception:
1998 log.exception("Exception occurred while trying edit permissions for repo")
1996 log.exception("Exception occurred while trying edit permissions for repo")
1999 raise JSONRPCError(
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 userid, repoid
1999 userid, repoid
2002 )
2000 )
2003 )
2001 )
2004
2002
2005
2003
2006 @jsonrpc_method()
2004 @jsonrpc_method()
2007 def revoke_user_permission(request, apiuser, repoid, userid):
2005 def revoke_user_permission(request, apiuser, repoid, userid):
2008 """
2006 """
2009 Revoke permission for a user on the specified repository.
2007 Revoke permission for a user on the specified repository.
2010
2008
2011 This command can only be run using an |authtoken| with admin
2009 This command can only be run using an |authtoken| with admin
2012 permissions on the |repo|.
2010 permissions on the |repo|.
2013
2011
2014 :param apiuser: This is filled automatically from the |authtoken|.
2012 :param apiuser: This is filled automatically from the |authtoken|.
2015 :type apiuser: AuthUser
2013 :type apiuser: AuthUser
2016 :param repoid: Set the repository name or repository ID.
2014 :param repoid: Set the repository name or repository ID.
2017 :type repoid: str or int
2015 :type repoid: str or int
2018 :param userid: Set the user name of revoked user.
2016 :param userid: Set the user name of revoked user.
2019 :type userid: str or int
2017 :type userid: str or int
2020
2018
2021 Example error output:
2019 Example error output:
2022
2020
2023 .. code-block:: bash
2021 .. code-block:: bash
2024
2022
2025 id : <id_given_in_input>
2023 id : <id_given_in_input>
2026 result: {
2024 result: {
2027 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
2025 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
2028 "success": true
2026 "success": true
2029 }
2027 }
2030 error: null
2028 error: null
2031 """
2029 """
2032
2030
2033 repo = get_repo_or_error(repoid)
2031 repo = get_repo_or_error(repoid)
2034 user = get_user_or_error(userid)
2032 user = get_user_or_error(userid)
2035 if not has_superadmin_permission(apiuser):
2033 if not has_superadmin_permission(apiuser):
2036 _perms = ('repository.admin',)
2034 _perms = ('repository.admin',)
2037 validate_repo_permissions(apiuser, repoid, repo, _perms)
2035 validate_repo_permissions(apiuser, repoid, repo, _perms)
2038
2036
2039 perm_deletions = [[user.user_id, None, "user"]]
2037 perm_deletions = [[user.user_id, None, "user"]]
2040 try:
2038 try:
2041 changes = RepoModel().update_permissions(
2039 changes = RepoModel().update_permissions(
2042 repo=repo, perm_deletions=perm_deletions, cur_user=user)
2040 repo=repo, perm_deletions=perm_deletions, cur_user=user)
2043
2041
2044 action_data = {
2042 action_data = {
2045 'added': changes['added'],
2043 'added': changes['added'],
2046 'updated': changes['updated'],
2044 'updated': changes['updated'],
2047 'deleted': changes['deleted'],
2045 'deleted': changes['deleted'],
2048 }
2046 }
2049 audit_logger.store_api(
2047 audit_logger.store_api(
2050 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2048 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2051 Session().commit()
2049 Session().commit()
2052 PermissionModel().flush_user_permission_caches(changes)
2050 PermissionModel().flush_user_permission_caches(changes)
2053
2051
2054 return {
2052 return {
2055 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
2053 'msg': 'Revoked perm for user: `{}` in repo: `{}`'.format(
2056 user.username, repo.repo_name
2054 user.username, repo.repo_name
2057 ),
2055 ),
2058 'success': True
2056 'success': True
2059 }
2057 }
2060 except Exception:
2058 except Exception:
2061 log.exception("Exception occurred while trying revoke permissions to repo")
2059 log.exception("Exception occurred while trying revoke permissions to repo")
2062 raise JSONRPCError(
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 userid, repoid
2062 userid, repoid
2065 )
2063 )
2066 )
2064 )
2067
2065
2068
2066
2069 @jsonrpc_method()
2067 @jsonrpc_method()
2070 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
2068 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
2071 """
2069 """
2072 Grant permission for a user group on the specified repository,
2070 Grant permission for a user group on the specified repository,
2073 or update existing permissions.
2071 or update existing permissions.
2074
2072
2075 This command can only be run using an |authtoken| with admin
2073 This command can only be run using an |authtoken| with admin
2076 permissions on the |repo|.
2074 permissions on the |repo|.
2077
2075
2078 :param apiuser: This is filled automatically from the |authtoken|.
2076 :param apiuser: This is filled automatically from the |authtoken|.
2079 :type apiuser: AuthUser
2077 :type apiuser: AuthUser
2080 :param repoid: Set the repository name or repository ID.
2078 :param repoid: Set the repository name or repository ID.
2081 :type repoid: str or int
2079 :type repoid: str or int
2082 :param usergroupid: Specify the ID of the user group.
2080 :param usergroupid: Specify the ID of the user group.
2083 :type usergroupid: str or int
2081 :type usergroupid: str or int
2084 :param perm: Set the user group permissions using the following
2082 :param perm: Set the user group permissions using the following
2085 format: (repository.(none|read|write|admin))
2083 format: (repository.(none|read|write|admin))
2086 :type perm: str
2084 :type perm: str
2087
2085
2088 Example output:
2086 Example output:
2089
2087
2090 .. code-block:: bash
2088 .. code-block:: bash
2091
2089
2092 id : <id_given_in_input>
2090 id : <id_given_in_input>
2093 result : {
2091 result : {
2094 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
2092 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
2095 "success": true
2093 "success": true
2096
2094
2097 }
2095 }
2098 error : null
2096 error : null
2099
2097
2100 Example error output:
2098 Example error output:
2101
2099
2102 .. code-block:: bash
2100 .. code-block:: bash
2103
2101
2104 id : <id_given_in_input>
2102 id : <id_given_in_input>
2105 result : null
2103 result : null
2106 error : {
2104 error : {
2107 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
2105 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
2108 }
2106 }
2109
2107
2110 """
2108 """
2111
2109
2112 repo = get_repo_or_error(repoid)
2110 repo = get_repo_or_error(repoid)
2113 perm = get_perm_or_error(perm)
2111 perm = get_perm_or_error(perm)
2114 if not has_superadmin_permission(apiuser):
2112 if not has_superadmin_permission(apiuser):
2115 _perms = ('repository.admin',)
2113 _perms = ('repository.admin',)
2116 validate_repo_permissions(apiuser, repoid, repo, _perms)
2114 validate_repo_permissions(apiuser, repoid, repo, _perms)
2117
2115
2118 user_group = get_user_group_or_error(usergroupid)
2116 user_group = get_user_group_or_error(usergroupid)
2119 if not has_superadmin_permission(apiuser):
2117 if not has_superadmin_permission(apiuser):
2120 # check if we have at least read permission for this user group !
2118 # check if we have at least read permission for this user group !
2121 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2119 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2122 if not HasUserGroupPermissionAnyApi(*_perms)(
2120 if not HasUserGroupPermissionAnyApi(*_perms)(
2123 user=apiuser, user_group_name=user_group.users_group_name):
2121 user=apiuser, user_group_name=user_group.users_group_name):
2124 raise JSONRPCError(
2122 raise JSONRPCError(
2125 'user group `%s` does not exist' % (usergroupid,))
2123 'user group `{}` does not exist'.format(usergroupid))
2126
2124
2127 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
2125 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
2128 try:
2126 try:
2129 changes = RepoModel().update_permissions(
2127 changes = RepoModel().update_permissions(
2130 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
2128 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
2131 action_data = {
2129 action_data = {
2132 'added': changes['added'],
2130 'added': changes['added'],
2133 'updated': changes['updated'],
2131 'updated': changes['updated'],
2134 'deleted': changes['deleted'],
2132 'deleted': changes['deleted'],
2135 }
2133 }
2136 audit_logger.store_api(
2134 audit_logger.store_api(
2137 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2135 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2138 Session().commit()
2136 Session().commit()
2139 PermissionModel().flush_user_permission_caches(changes)
2137 PermissionModel().flush_user_permission_caches(changes)
2140
2138
2141 return {
2139 return {
2142 'msg': 'Granted perm: `%s` for user group: `%s` in '
2140 'msg': 'Granted perm: `%s` for user group: `%s` in '
2143 'repo: `%s`' % (
2141 'repo: `%s`' % (
2144 perm.permission_name, user_group.users_group_name,
2142 perm.permission_name, user_group.users_group_name,
2145 repo.repo_name
2143 repo.repo_name
2146 ),
2144 ),
2147 'success': True
2145 'success': True
2148 }
2146 }
2149 except Exception:
2147 except Exception:
2150 log.exception(
2148 log.exception(
2151 "Exception occurred while trying change permission on repo")
2149 "Exception occurred while trying change permission on repo")
2152 raise JSONRPCError(
2150 raise JSONRPCError(
2153 'failed to edit permission for user group: `%s` in '
2151 'failed to edit permission for user group: `%s` in '
2154 'repo: `%s`' % (
2152 'repo: `%s`' % (
2155 usergroupid, repo.repo_name
2153 usergroupid, repo.repo_name
2156 )
2154 )
2157 )
2155 )
2158
2156
2159
2157
2160 @jsonrpc_method()
2158 @jsonrpc_method()
2161 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
2159 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
2162 """
2160 """
2163 Revoke the permissions of a user group on a given repository.
2161 Revoke the permissions of a user group on a given repository.
2164
2162
2165 This command can only be run using an |authtoken| with admin
2163 This command can only be run using an |authtoken| with admin
2166 permissions on the |repo|.
2164 permissions on the |repo|.
2167
2165
2168 :param apiuser: This is filled automatically from the |authtoken|.
2166 :param apiuser: This is filled automatically from the |authtoken|.
2169 :type apiuser: AuthUser
2167 :type apiuser: AuthUser
2170 :param repoid: Set the repository name or repository ID.
2168 :param repoid: Set the repository name or repository ID.
2171 :type repoid: str or int
2169 :type repoid: str or int
2172 :param usergroupid: Specify the user group ID.
2170 :param usergroupid: Specify the user group ID.
2173 :type usergroupid: str or int
2171 :type usergroupid: str or int
2174
2172
2175 Example output:
2173 Example output:
2176
2174
2177 .. code-block:: bash
2175 .. code-block:: bash
2178
2176
2179 id : <id_given_in_input>
2177 id : <id_given_in_input>
2180 result: {
2178 result: {
2181 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
2179 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
2182 "success": true
2180 "success": true
2183 }
2181 }
2184 error: null
2182 error: null
2185 """
2183 """
2186
2184
2187 repo = get_repo_or_error(repoid)
2185 repo = get_repo_or_error(repoid)
2188 if not has_superadmin_permission(apiuser):
2186 if not has_superadmin_permission(apiuser):
2189 _perms = ('repository.admin',)
2187 _perms = ('repository.admin',)
2190 validate_repo_permissions(apiuser, repoid, repo, _perms)
2188 validate_repo_permissions(apiuser, repoid, repo, _perms)
2191
2189
2192 user_group = get_user_group_or_error(usergroupid)
2190 user_group = get_user_group_or_error(usergroupid)
2193 if not has_superadmin_permission(apiuser):
2191 if not has_superadmin_permission(apiuser):
2194 # check if we have at least read permission for this user group !
2192 # check if we have at least read permission for this user group !
2195 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2193 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2196 if not HasUserGroupPermissionAnyApi(*_perms)(
2194 if not HasUserGroupPermissionAnyApi(*_perms)(
2197 user=apiuser, user_group_name=user_group.users_group_name):
2195 user=apiuser, user_group_name=user_group.users_group_name):
2198 raise JSONRPCError(
2196 raise JSONRPCError(
2199 'user group `%s` does not exist' % (usergroupid,))
2197 'user group `{}` does not exist'.format(usergroupid))
2200
2198
2201 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2199 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2202 try:
2200 try:
2203 changes = RepoModel().update_permissions(
2201 changes = RepoModel().update_permissions(
2204 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2202 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2205 action_data = {
2203 action_data = {
2206 'added': changes['added'],
2204 'added': changes['added'],
2207 'updated': changes['updated'],
2205 'updated': changes['updated'],
2208 'deleted': changes['deleted'],
2206 'deleted': changes['deleted'],
2209 }
2207 }
2210 audit_logger.store_api(
2208 audit_logger.store_api(
2211 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2209 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2212 Session().commit()
2210 Session().commit()
2213 PermissionModel().flush_user_permission_caches(changes)
2211 PermissionModel().flush_user_permission_caches(changes)
2214
2212
2215 return {
2213 return {
2216 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2214 'msg': 'Revoked perm for user group: `{}` in repo: `{}`'.format(
2217 user_group.users_group_name, repo.repo_name
2215 user_group.users_group_name, repo.repo_name
2218 ),
2216 ),
2219 'success': True
2217 'success': True
2220 }
2218 }
2221 except Exception:
2219 except Exception:
2222 log.exception("Exception occurred while trying revoke "
2220 log.exception("Exception occurred while trying revoke "
2223 "user group permission on repo")
2221 "user group permission on repo")
2224 raise JSONRPCError(
2222 raise JSONRPCError(
2225 'failed to edit permission for user group: `%s` in '
2223 'failed to edit permission for user group: `%s` in '
2226 'repo: `%s`' % (
2224 'repo: `%s`' % (
2227 user_group.users_group_name, repo.repo_name
2225 user_group.users_group_name, repo.repo_name
2228 )
2226 )
2229 )
2227 )
2230
2228
2231
2229
2232 @jsonrpc_method()
2230 @jsonrpc_method()
2233 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2231 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2234 """
2232 """
2235 Triggers a pull on the given repository from a remote location. You
2233 Triggers a pull on the given repository from a remote location. You
2236 can use this to keep remote repositories up-to-date.
2234 can use this to keep remote repositories up-to-date.
2237
2235
2238 This command can only be run using an |authtoken| with admin
2236 This command can only be run using an |authtoken| with admin
2239 rights to the specified repository. For more information,
2237 rights to the specified repository. For more information,
2240 see :ref:`config-token-ref`.
2238 see :ref:`config-token-ref`.
2241
2239
2242 This command takes the following options:
2240 This command takes the following options:
2243
2241
2244 :param apiuser: This is filled automatically from the |authtoken|.
2242 :param apiuser: This is filled automatically from the |authtoken|.
2245 :type apiuser: AuthUser
2243 :type apiuser: AuthUser
2246 :param repoid: The repository name or repository ID.
2244 :param repoid: The repository name or repository ID.
2247 :type repoid: str or int
2245 :type repoid: str or int
2248 :param remote_uri: Optional remote URI to pass in for pull
2246 :param remote_uri: Optional remote URI to pass in for pull
2249 :type remote_uri: str
2247 :type remote_uri: str
2250
2248
2251 Example output:
2249 Example output:
2252
2250
2253 .. code-block:: bash
2251 .. code-block:: bash
2254
2252
2255 id : <id_given_in_input>
2253 id : <id_given_in_input>
2256 result : {
2254 result : {
2257 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2255 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2258 "repository": "<repository name>"
2256 "repository": "<repository name>"
2259 }
2257 }
2260 error : null
2258 error : null
2261
2259
2262 Example error output:
2260 Example error output:
2263
2261
2264 .. code-block:: bash
2262 .. code-block:: bash
2265
2263
2266 id : <id_given_in_input>
2264 id : <id_given_in_input>
2267 result : null
2265 result : null
2268 error : {
2266 error : {
2269 "Unable to push changes from `<remote_url>`"
2267 "Unable to push changes from `<remote_url>`"
2270 }
2268 }
2271
2269
2272 """
2270 """
2273
2271
2274 repo = get_repo_or_error(repoid)
2272 repo = get_repo_or_error(repoid)
2275 remote_uri = Optional.extract(remote_uri)
2273 remote_uri = Optional.extract(remote_uri)
2276 remote_uri_display = remote_uri or repo.clone_uri_hidden
2274 remote_uri_display = remote_uri or repo.clone_uri_hidden
2277 if not has_superadmin_permission(apiuser):
2275 if not has_superadmin_permission(apiuser):
2278 _perms = ('repository.admin',)
2276 _perms = ('repository.admin',)
2279 validate_repo_permissions(apiuser, repoid, repo, _perms)
2277 validate_repo_permissions(apiuser, repoid, repo, _perms)
2280
2278
2281 try:
2279 try:
2282 ScmModel().pull_changes(
2280 ScmModel().pull_changes(
2283 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2281 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2284 return {
2282 return {
2285 'msg': 'Pulled from url `%s` on repo `%s`' % (
2283 'msg': 'Pulled from url `{}` on repo `{}`'.format(
2286 remote_uri_display, repo.repo_name),
2284 remote_uri_display, repo.repo_name),
2287 'repository': repo.repo_name
2285 'repository': repo.repo_name
2288 }
2286 }
2289 except Exception:
2287 except Exception:
2290 log.exception("Exception occurred while trying to "
2288 log.exception("Exception occurred while trying to "
2291 "pull changes from remote location")
2289 "pull changes from remote location")
2292 raise JSONRPCError(
2290 raise JSONRPCError(
2293 'Unable to pull changes from `%s`' % remote_uri_display
2291 'Unable to pull changes from `%s`' % remote_uri_display
2294 )
2292 )
2295
2293
2296
2294
2297 @jsonrpc_method()
2295 @jsonrpc_method()
2298 def strip(request, apiuser, repoid, revision, branch):
2296 def strip(request, apiuser, repoid, revision, branch):
2299 """
2297 """
2300 Strips the given revision from the specified repository.
2298 Strips the given revision from the specified repository.
2301
2299
2302 * This will remove the revision and all of its decendants.
2300 * This will remove the revision and all of its decendants.
2303
2301
2304 This command can only be run using an |authtoken| with admin rights to
2302 This command can only be run using an |authtoken| with admin rights to
2305 the specified repository.
2303 the specified repository.
2306
2304
2307 This command takes the following options:
2305 This command takes the following options:
2308
2306
2309 :param apiuser: This is filled automatically from the |authtoken|.
2307 :param apiuser: This is filled automatically from the |authtoken|.
2310 :type apiuser: AuthUser
2308 :type apiuser: AuthUser
2311 :param repoid: The repository name or repository ID.
2309 :param repoid: The repository name or repository ID.
2312 :type repoid: str or int
2310 :type repoid: str or int
2313 :param revision: The revision you wish to strip.
2311 :param revision: The revision you wish to strip.
2314 :type revision: str
2312 :type revision: str
2315 :param branch: The branch from which to strip the revision.
2313 :param branch: The branch from which to strip the revision.
2316 :type branch: str
2314 :type branch: str
2317
2315
2318 Example output:
2316 Example output:
2319
2317
2320 .. code-block:: bash
2318 .. code-block:: bash
2321
2319
2322 id : <id_given_in_input>
2320 id : <id_given_in_input>
2323 result : {
2321 result : {
2324 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2322 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2325 "repository": "<repository name>"
2323 "repository": "<repository name>"
2326 }
2324 }
2327 error : null
2325 error : null
2328
2326
2329 Example error output:
2327 Example error output:
2330
2328
2331 .. code-block:: bash
2329 .. code-block:: bash
2332
2330
2333 id : <id_given_in_input>
2331 id : <id_given_in_input>
2334 result : null
2332 result : null
2335 error : {
2333 error : {
2336 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2334 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2337 }
2335 }
2338
2336
2339 """
2337 """
2340
2338
2341 repo = get_repo_or_error(repoid)
2339 repo = get_repo_or_error(repoid)
2342 if not has_superadmin_permission(apiuser):
2340 if not has_superadmin_permission(apiuser):
2343 _perms = ('repository.admin',)
2341 _perms = ('repository.admin',)
2344 validate_repo_permissions(apiuser, repoid, repo, _perms)
2342 validate_repo_permissions(apiuser, repoid, repo, _perms)
2345
2343
2346 try:
2344 try:
2347 ScmModel().strip(repo, revision, branch)
2345 ScmModel().strip(repo, revision, branch)
2348 audit_logger.store_api(
2346 audit_logger.store_api(
2349 'repo.commit.strip', action_data={'commit_id': revision},
2347 'repo.commit.strip', action_data={'commit_id': revision},
2350 repo=repo,
2348 repo=repo,
2351 user=apiuser, commit=True)
2349 user=apiuser, commit=True)
2352
2350
2353 return {
2351 return {
2354 'msg': 'Stripped commit %s from repo `%s`' % (
2352 'msg': 'Stripped commit {} from repo `{}`'.format(
2355 revision, repo.repo_name),
2353 revision, repo.repo_name),
2356 'repository': repo.repo_name
2354 'repository': repo.repo_name
2357 }
2355 }
2358 except Exception:
2356 except Exception:
2359 log.exception("Exception while trying to strip")
2357 log.exception("Exception while trying to strip")
2360 raise JSONRPCError(
2358 raise JSONRPCError(
2361 'Unable to strip commit %s from repo `%s`' % (
2359 'Unable to strip commit {} from repo `{}`'.format(
2362 revision, repo.repo_name)
2360 revision, repo.repo_name)
2363 )
2361 )
2364
2362
2365
2363
2366 @jsonrpc_method()
2364 @jsonrpc_method()
2367 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2365 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2368 """
2366 """
2369 Returns all settings for a repository. If key is given it only returns the
2367 Returns all settings for a repository. If key is given it only returns the
2370 setting identified by the key or null.
2368 setting identified by the key or null.
2371
2369
2372 :param apiuser: This is filled automatically from the |authtoken|.
2370 :param apiuser: This is filled automatically from the |authtoken|.
2373 :type apiuser: AuthUser
2371 :type apiuser: AuthUser
2374 :param repoid: The repository name or repository id.
2372 :param repoid: The repository name or repository id.
2375 :type repoid: str or int
2373 :type repoid: str or int
2376 :param key: Key of the setting to return.
2374 :param key: Key of the setting to return.
2377 :type: key: Optional(str)
2375 :type: key: Optional(str)
2378
2376
2379 Example output:
2377 Example output:
2380
2378
2381 .. code-block:: bash
2379 .. code-block:: bash
2382
2380
2383 {
2381 {
2384 "error": null,
2382 "error": null,
2385 "id": 237,
2383 "id": 237,
2386 "result": {
2384 "result": {
2387 "extensions_largefiles": true,
2385 "extensions_largefiles": true,
2388 "extensions_evolve": true,
2386 "extensions_evolve": true,
2389 "hooks_changegroup_push_logger": true,
2387 "hooks_changegroup_push_logger": true,
2390 "hooks_changegroup_repo_size": false,
2388 "hooks_changegroup_repo_size": false,
2391 "hooks_outgoing_pull_logger": true,
2389 "hooks_outgoing_pull_logger": true,
2392 "phases_publish": "True",
2390 "phases_publish": "True",
2393 "rhodecode_hg_use_rebase_for_merging": true,
2391 "rhodecode_hg_use_rebase_for_merging": true,
2394 "rhodecode_pr_merge_enabled": true,
2392 "rhodecode_pr_merge_enabled": true,
2395 "rhodecode_use_outdated_comments": true
2393 "rhodecode_use_outdated_comments": true
2396 }
2394 }
2397 }
2395 }
2398 """
2396 """
2399
2397
2400 # Restrict access to this api method to super-admins, and repo admins only.
2398 # Restrict access to this api method to super-admins, and repo admins only.
2401 repo = get_repo_or_error(repoid)
2399 repo = get_repo_or_error(repoid)
2402 if not has_superadmin_permission(apiuser):
2400 if not has_superadmin_permission(apiuser):
2403 _perms = ('repository.admin',)
2401 _perms = ('repository.admin',)
2404 validate_repo_permissions(apiuser, repoid, repo, _perms)
2402 validate_repo_permissions(apiuser, repoid, repo, _perms)
2405
2403
2406 try:
2404 try:
2407 settings_model = VcsSettingsModel(repo=repo)
2405 settings_model = VcsSettingsModel(repo=repo)
2408 settings = settings_model.get_global_settings()
2406 settings = settings_model.get_global_settings()
2409 settings.update(settings_model.get_repo_settings())
2407 settings.update(settings_model.get_repo_settings())
2410
2408
2411 # If only a single setting is requested fetch it from all settings.
2409 # If only a single setting is requested fetch it from all settings.
2412 key = Optional.extract(key)
2410 key = Optional.extract(key)
2413 if key is not None:
2411 if key is not None:
2414 settings = settings.get(key, None)
2412 settings = settings.get(key, None)
2415 except Exception:
2413 except Exception:
2416 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2414 msg = f'Failed to fetch settings for repository `{repoid}`'
2417 log.exception(msg)
2415 log.exception(msg)
2418 raise JSONRPCError(msg)
2416 raise JSONRPCError(msg)
2419
2417
2420 return settings
2418 return settings
2421
2419
2422
2420
2423 @jsonrpc_method()
2421 @jsonrpc_method()
2424 def set_repo_settings(request, apiuser, repoid, settings):
2422 def set_repo_settings(request, apiuser, repoid, settings):
2425 """
2423 """
2426 Update repository settings. Returns true on success.
2424 Update repository settings. Returns true on success.
2427
2425
2428 :param apiuser: This is filled automatically from the |authtoken|.
2426 :param apiuser: This is filled automatically from the |authtoken|.
2429 :type apiuser: AuthUser
2427 :type apiuser: AuthUser
2430 :param repoid: The repository name or repository id.
2428 :param repoid: The repository name or repository id.
2431 :type repoid: str or int
2429 :type repoid: str or int
2432 :param settings: The new settings for the repository.
2430 :param settings: The new settings for the repository.
2433 :type: settings: dict
2431 :type: settings: dict
2434
2432
2435 Example output:
2433 Example output:
2436
2434
2437 .. code-block:: bash
2435 .. code-block:: bash
2438
2436
2439 {
2437 {
2440 "error": null,
2438 "error": null,
2441 "id": 237,
2439 "id": 237,
2442 "result": true
2440 "result": true
2443 }
2441 }
2444 """
2442 """
2445 # Restrict access to this api method to super-admins, and repo admins only.
2443 # Restrict access to this api method to super-admins, and repo admins only.
2446 repo = get_repo_or_error(repoid)
2444 repo = get_repo_or_error(repoid)
2447 if not has_superadmin_permission(apiuser):
2445 if not has_superadmin_permission(apiuser):
2448 _perms = ('repository.admin',)
2446 _perms = ('repository.admin',)
2449 validate_repo_permissions(apiuser, repoid, repo, _perms)
2447 validate_repo_permissions(apiuser, repoid, repo, _perms)
2450
2448
2451 if type(settings) is not dict:
2449 if type(settings) is not dict:
2452 raise JSONRPCError('Settings have to be a JSON Object.')
2450 raise JSONRPCError('Settings have to be a JSON Object.')
2453
2451
2454 try:
2452 try:
2455 settings_model = VcsSettingsModel(repo=repoid)
2453 settings_model = VcsSettingsModel(repo=repoid)
2456
2454
2457 # Merge global, repo and incoming settings.
2455 # Merge global, repo and incoming settings.
2458 new_settings = settings_model.get_global_settings()
2456 new_settings = settings_model.get_global_settings()
2459 new_settings.update(settings_model.get_repo_settings())
2457 new_settings.update(settings_model.get_repo_settings())
2460 new_settings.update(settings)
2458 new_settings.update(settings)
2461
2459
2462 # Update the settings.
2460 # Update the settings.
2463 inherit_global_settings = new_settings.get(
2461 inherit_global_settings = new_settings.get(
2464 'inherit_global_settings', False)
2462 'inherit_global_settings', False)
2465 settings_model.create_or_update_repo_settings(
2463 settings_model.create_or_update_repo_settings(
2466 new_settings, inherit_global_settings=inherit_global_settings)
2464 new_settings, inherit_global_settings=inherit_global_settings)
2467 Session().commit()
2465 Session().commit()
2468 except Exception:
2466 except Exception:
2469 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2467 msg = f'Failed to update settings for repository `{repoid}`'
2470 log.exception(msg)
2468 log.exception(msg)
2471 raise JSONRPCError(msg)
2469 raise JSONRPCError(msg)
2472
2470
2473 # Indicate success.
2471 # Indicate success.
2474 return True
2472 return True
2475
2473
2476
2474
2477 @jsonrpc_method()
2475 @jsonrpc_method()
2478 def maintenance(request, apiuser, repoid):
2476 def maintenance(request, apiuser, repoid):
2479 """
2477 """
2480 Triggers a maintenance on the given repository.
2478 Triggers a maintenance on the given repository.
2481
2479
2482 This command can only be run using an |authtoken| with admin
2480 This command can only be run using an |authtoken| with admin
2483 rights to the specified repository. For more information,
2481 rights to the specified repository. For more information,
2484 see :ref:`config-token-ref`.
2482 see :ref:`config-token-ref`.
2485
2483
2486 This command takes the following options:
2484 This command takes the following options:
2487
2485
2488 :param apiuser: This is filled automatically from the |authtoken|.
2486 :param apiuser: This is filled automatically from the |authtoken|.
2489 :type apiuser: AuthUser
2487 :type apiuser: AuthUser
2490 :param repoid: The repository name or repository ID.
2488 :param repoid: The repository name or repository ID.
2491 :type repoid: str or int
2489 :type repoid: str or int
2492
2490
2493 Example output:
2491 Example output:
2494
2492
2495 .. code-block:: bash
2493 .. code-block:: bash
2496
2494
2497 id : <id_given_in_input>
2495 id : <id_given_in_input>
2498 result : {
2496 result : {
2499 "msg": "executed maintenance command",
2497 "msg": "executed maintenance command",
2500 "executed_actions": [
2498 "executed_actions": [
2501 <action_message>, <action_message2>...
2499 <action_message>, <action_message2>...
2502 ],
2500 ],
2503 "repository": "<repository name>"
2501 "repository": "<repository name>"
2504 }
2502 }
2505 error : null
2503 error : null
2506
2504
2507 Example error output:
2505 Example error output:
2508
2506
2509 .. code-block:: bash
2507 .. code-block:: bash
2510
2508
2511 id : <id_given_in_input>
2509 id : <id_given_in_input>
2512 result : null
2510 result : null
2513 error : {
2511 error : {
2514 "Unable to execute maintenance on `<reponame>`"
2512 "Unable to execute maintenance on `<reponame>`"
2515 }
2513 }
2516
2514
2517 """
2515 """
2518
2516
2519 repo = get_repo_or_error(repoid)
2517 repo = get_repo_or_error(repoid)
2520 if not has_superadmin_permission(apiuser):
2518 if not has_superadmin_permission(apiuser):
2521 _perms = ('repository.admin',)
2519 _perms = ('repository.admin',)
2522 validate_repo_permissions(apiuser, repoid, repo, _perms)
2520 validate_repo_permissions(apiuser, repoid, repo, _perms)
2523
2521
2524 try:
2522 try:
2525 maintenance = repo_maintenance.RepoMaintenance()
2523 maintenance = repo_maintenance.RepoMaintenance()
2526 executed_actions = maintenance.execute(repo)
2524 executed_actions = maintenance.execute(repo)
2527
2525
2528 return {
2526 return {
2529 'msg': 'executed maintenance command',
2527 'msg': 'executed maintenance command',
2530 'executed_actions': executed_actions,
2528 'executed_actions': executed_actions,
2531 'repository': repo.repo_name
2529 'repository': repo.repo_name
2532 }
2530 }
2533 except Exception:
2531 except Exception:
2534 log.exception("Exception occurred while trying to run maintenance")
2532 log.exception("Exception occurred while trying to run maintenance")
2535 raise JSONRPCError(
2533 raise JSONRPCError(
2536 'Unable to execute maintenance on `%s`' % repo.repo_name)
2534 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,762 +1,760 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 import logging
20 import logging
23
21
24 from rhodecode.api import JSONRPCValidationError
22 from rhodecode.api import JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError
23 from rhodecode.api import jsonrpc_method, JSONRPCError
26 from rhodecode.api.utils import (
24 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_user_or_error,
25 has_superadmin_permission, Optional, OAttr, get_user_or_error,
28 get_repo_group_or_error, get_perm_or_error, get_user_group_or_error,
26 get_repo_group_or_error, get_perm_or_error, get_user_group_or_error,
29 get_origin, validate_repo_group_permissions, validate_set_owner_permissions)
27 get_origin, validate_repo_group_permissions, validate_set_owner_permissions)
30 from rhodecode.lib import audit_logger
28 from rhodecode.lib import audit_logger
31 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
32 HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi)
30 HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi)
33 from rhodecode.model.db import Session
31 from rhodecode.model.db import Session
34 from rhodecode.model.permission import PermissionModel
32 from rhodecode.model.permission import PermissionModel
35 from rhodecode.model.repo_group import RepoGroupModel
33 from rhodecode.model.repo_group import RepoGroupModel
36 from rhodecode.model.scm import RepoGroupList
34 from rhodecode.model.scm import RepoGroupList
37 from rhodecode.model import validation_schema
35 from rhodecode.model import validation_schema
38 from rhodecode.model.validation_schema.schemas import repo_group_schema
36 from rhodecode.model.validation_schema.schemas import repo_group_schema
39
37
40
38
41 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
42
40
43
41
44 @jsonrpc_method()
42 @jsonrpc_method()
45 def get_repo_group(request, apiuser, repogroupid):
43 def get_repo_group(request, apiuser, repogroupid):
46 """
44 """
47 Return the specified |repo| group, along with permissions,
45 Return the specified |repo| group, along with permissions,
48 and repositories inside the group
46 and repositories inside the group
49
47
50 :param apiuser: This is filled automatically from the |authtoken|.
48 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
49 :type apiuser: AuthUser
52 :param repogroupid: Specify the name of ID of the repository group.
50 :param repogroupid: Specify the name of ID of the repository group.
53 :type repogroupid: str or int
51 :type repogroupid: str or int
54
52
55
53
56 Example output:
54 Example output:
57
55
58 .. code-block:: bash
56 .. code-block:: bash
59
57
60 {
58 {
61 "error": null,
59 "error": null,
62 "id": repo-group-id,
60 "id": repo-group-id,
63 "result": {
61 "result": {
64 "group_description": "repo group description",
62 "group_description": "repo group description",
65 "group_id": 14,
63 "group_id": 14,
66 "group_name": "group name",
64 "group_name": "group name",
67 "permissions": [
65 "permissions": [
68 {
66 {
69 "name": "super-admin-username",
67 "name": "super-admin-username",
70 "origin": "super-admin",
68 "origin": "super-admin",
71 "permission": "group.admin",
69 "permission": "group.admin",
72 "type": "user"
70 "type": "user"
73 },
71 },
74 {
72 {
75 "name": "owner-name",
73 "name": "owner-name",
76 "origin": "owner",
74 "origin": "owner",
77 "permission": "group.admin",
75 "permission": "group.admin",
78 "type": "user"
76 "type": "user"
79 },
77 },
80 {
78 {
81 "name": "user-group-name",
79 "name": "user-group-name",
82 "origin": "permission",
80 "origin": "permission",
83 "permission": "group.write",
81 "permission": "group.write",
84 "type": "user_group"
82 "type": "user_group"
85 }
83 }
86 ],
84 ],
87 "owner": "owner-name",
85 "owner": "owner-name",
88 "parent_group": null,
86 "parent_group": null,
89 "repositories": [ repo-list ]
87 "repositories": [ repo-list ]
90 }
88 }
91 }
89 }
92 """
90 """
93
91
94 repo_group = get_repo_group_or_error(repogroupid)
92 repo_group = get_repo_group_or_error(repogroupid)
95 if not has_superadmin_permission(apiuser):
93 if not has_superadmin_permission(apiuser):
96 # check if we have at least read permission for this repo group !
94 # check if we have at least read permission for this repo group !
97 _perms = ('group.admin', 'group.write', 'group.read',)
95 _perms = ('group.admin', 'group.write', 'group.read',)
98 if not HasRepoGroupPermissionAnyApi(*_perms)(
96 if not HasRepoGroupPermissionAnyApi(*_perms)(
99 user=apiuser, group_name=repo_group.group_name):
97 user=apiuser, group_name=repo_group.group_name):
100 raise JSONRPCError(
98 raise JSONRPCError(
101 'repository group `%s` does not exist' % (repogroupid,))
99 'repository group `{}` does not exist'.format(repogroupid))
102
100
103 permissions = []
101 permissions = []
104 for _user in repo_group.permissions():
102 for _user in repo_group.permissions():
105 user_data = {
103 user_data = {
106 'name': _user.username,
104 'name': _user.username,
107 'permission': _user.permission,
105 'permission': _user.permission,
108 'origin': get_origin(_user),
106 'origin': get_origin(_user),
109 'type': "user",
107 'type': "user",
110 }
108 }
111 permissions.append(user_data)
109 permissions.append(user_data)
112
110
113 for _user_group in repo_group.permission_user_groups():
111 for _user_group in repo_group.permission_user_groups():
114 user_group_data = {
112 user_group_data = {
115 'name': _user_group.users_group_name,
113 'name': _user_group.users_group_name,
116 'permission': _user_group.permission,
114 'permission': _user_group.permission,
117 'origin': get_origin(_user_group),
115 'origin': get_origin(_user_group),
118 'type': "user_group",
116 'type': "user_group",
119 }
117 }
120 permissions.append(user_group_data)
118 permissions.append(user_group_data)
121
119
122 data = repo_group.get_api_data()
120 data = repo_group.get_api_data()
123 data["permissions"] = permissions
121 data["permissions"] = permissions
124 return data
122 return data
125
123
126
124
127 @jsonrpc_method()
125 @jsonrpc_method()
128 def get_repo_groups(request, apiuser):
126 def get_repo_groups(request, apiuser):
129 """
127 """
130 Returns all repository groups.
128 Returns all repository groups.
131
129
132 :param apiuser: This is filled automatically from the |authtoken|.
130 :param apiuser: This is filled automatically from the |authtoken|.
133 :type apiuser: AuthUser
131 :type apiuser: AuthUser
134 """
132 """
135
133
136 result = []
134 result = []
137 _perms = ('group.read', 'group.write', 'group.admin',)
135 _perms = ('group.read', 'group.write', 'group.admin',)
138 extras = {'user': apiuser}
136 extras = {'user': apiuser}
139 for repo_group in RepoGroupList(RepoGroupModel().get_all(),
137 for repo_group in RepoGroupList(RepoGroupModel().get_all(),
140 perm_set=_perms, extra_kwargs=extras):
138 perm_set=_perms, extra_kwargs=extras):
141 result.append(repo_group.get_api_data())
139 result.append(repo_group.get_api_data())
142 return result
140 return result
143
141
144
142
145 @jsonrpc_method()
143 @jsonrpc_method()
146 def create_repo_group(
144 def create_repo_group(
147 request, apiuser, group_name,
145 request, apiuser, group_name,
148 owner=Optional(OAttr('apiuser')),
146 owner=Optional(OAttr('apiuser')),
149 description=Optional(''),
147 description=Optional(''),
150 copy_permissions=Optional(False)):
148 copy_permissions=Optional(False)):
151 """
149 """
152 Creates a repository group.
150 Creates a repository group.
153
151
154 * If the repository group name contains "/", repository group will be
152 * If the repository group name contains "/", repository group will be
155 created inside a repository group or nested repository groups
153 created inside a repository group or nested repository groups
156
154
157 For example "foo/bar/group1" will create repository group called "group1"
155 For example "foo/bar/group1" will create repository group called "group1"
158 inside group "foo/bar". You have to have permissions to access and
156 inside group "foo/bar". You have to have permissions to access and
159 write to the last repository group ("bar" in this example)
157 write to the last repository group ("bar" in this example)
160
158
161 This command can only be run using an |authtoken| with at least
159 This command can only be run using an |authtoken| with at least
162 permissions to create repository groups, or admin permissions to
160 permissions to create repository groups, or admin permissions to
163 parent repository groups.
161 parent repository groups.
164
162
165 :param apiuser: This is filled automatically from the |authtoken|.
163 :param apiuser: This is filled automatically from the |authtoken|.
166 :type apiuser: AuthUser
164 :type apiuser: AuthUser
167 :param group_name: Set the repository group name.
165 :param group_name: Set the repository group name.
168 :type group_name: str
166 :type group_name: str
169 :param description: Set the |repo| group description.
167 :param description: Set the |repo| group description.
170 :type description: str
168 :type description: str
171 :param owner: Set the |repo| group owner.
169 :param owner: Set the |repo| group owner.
172 :type owner: str
170 :type owner: str
173 :param copy_permissions:
171 :param copy_permissions:
174 :type copy_permissions:
172 :type copy_permissions:
175
173
176 Example output:
174 Example output:
177
175
178 .. code-block:: bash
176 .. code-block:: bash
179
177
180 id : <id_given_in_input>
178 id : <id_given_in_input>
181 result : {
179 result : {
182 "msg": "Created new repo group `<repo_group_name>`"
180 "msg": "Created new repo group `<repo_group_name>`"
183 "repo_group": <repogroup_object>
181 "repo_group": <repogroup_object>
184 }
182 }
185 error : null
183 error : null
186
184
187
185
188 Example error output:
186 Example error output:
189
187
190 .. code-block:: bash
188 .. code-block:: bash
191
189
192 id : <id_given_in_input>
190 id : <id_given_in_input>
193 result : null
191 result : null
194 error : {
192 error : {
195 failed to create repo group `<repogroupid>`
193 failed to create repo group `<repogroupid>`
196 }
194 }
197
195
198 """
196 """
199
197
200 owner = validate_set_owner_permissions(apiuser, owner)
198 owner = validate_set_owner_permissions(apiuser, owner)
201
199
202 description = Optional.extract(description)
200 description = Optional.extract(description)
203 copy_permissions = Optional.extract(copy_permissions)
201 copy_permissions = Optional.extract(copy_permissions)
204
202
205 schema = repo_group_schema.RepoGroupSchema().bind(
203 schema = repo_group_schema.RepoGroupSchema().bind(
206 # user caller
204 # user caller
207 user=apiuser)
205 user=apiuser)
208
206
209 try:
207 try:
210 schema_data = schema.deserialize(dict(
208 schema_data = schema.deserialize(dict(
211 repo_group_name=group_name,
209 repo_group_name=group_name,
212 repo_group_owner=owner.username,
210 repo_group_owner=owner.username,
213 repo_group_description=description,
211 repo_group_description=description,
214 repo_group_copy_permissions=copy_permissions,
212 repo_group_copy_permissions=copy_permissions,
215 ))
213 ))
216 except validation_schema.Invalid as err:
214 except validation_schema.Invalid as err:
217 raise JSONRPCValidationError(colander_exc=err)
215 raise JSONRPCValidationError(colander_exc=err)
218
216
219 validated_group_name = schema_data['repo_group_name']
217 validated_group_name = schema_data['repo_group_name']
220
218
221 try:
219 try:
222 repo_group = RepoGroupModel().create(
220 repo_group = RepoGroupModel().create(
223 owner=owner,
221 owner=owner,
224 group_name=validated_group_name,
222 group_name=validated_group_name,
225 group_description=schema_data['repo_group_description'],
223 group_description=schema_data['repo_group_description'],
226 copy_permissions=schema_data['repo_group_copy_permissions'])
224 copy_permissions=schema_data['repo_group_copy_permissions'])
227 Session().flush()
225 Session().flush()
228
226
229 repo_group_data = repo_group.get_api_data()
227 repo_group_data = repo_group.get_api_data()
230 audit_logger.store_api(
228 audit_logger.store_api(
231 'repo_group.create', action_data={'data': repo_group_data},
229 'repo_group.create', action_data={'data': repo_group_data},
232 user=apiuser)
230 user=apiuser)
233
231
234 Session().commit()
232 Session().commit()
235
233
236 PermissionModel().trigger_permission_flush()
234 PermissionModel().trigger_permission_flush()
237
235
238 return {
236 return {
239 'msg': 'Created new repo group `%s`' % validated_group_name,
237 'msg': 'Created new repo group `%s`' % validated_group_name,
240 'repo_group': repo_group.get_api_data()
238 'repo_group': repo_group.get_api_data()
241 }
239 }
242 except Exception:
240 except Exception:
243 log.exception("Exception occurred while trying create repo group")
241 log.exception("Exception occurred while trying create repo group")
244 raise JSONRPCError(
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 @jsonrpc_method()
246 @jsonrpc_method()
249 def update_repo_group(
247 def update_repo_group(
250 request, apiuser, repogroupid, group_name=Optional(''),
248 request, apiuser, repogroupid, group_name=Optional(''),
251 description=Optional(''), owner=Optional(OAttr('apiuser')),
249 description=Optional(''), owner=Optional(OAttr('apiuser')),
252 enable_locking=Optional(False)):
250 enable_locking=Optional(False)):
253 """
251 """
254 Updates repository group with the details given.
252 Updates repository group with the details given.
255
253
256 This command can only be run using an |authtoken| with admin
254 This command can only be run using an |authtoken| with admin
257 permissions.
255 permissions.
258
256
259 * If the group_name name contains "/", repository group will be updated
257 * If the group_name name contains "/", repository group will be updated
260 accordingly with a repository group or nested repository groups
258 accordingly with a repository group or nested repository groups
261
259
262 For example repogroupid=group-test group_name="foo/bar/group-test"
260 For example repogroupid=group-test group_name="foo/bar/group-test"
263 will update repository group called "group-test" and place it
261 will update repository group called "group-test" and place it
264 inside group "foo/bar".
262 inside group "foo/bar".
265 You have to have permissions to access and write to the last repository
263 You have to have permissions to access and write to the last repository
266 group ("bar" in this example)
264 group ("bar" in this example)
267
265
268 :param apiuser: This is filled automatically from the |authtoken|.
266 :param apiuser: This is filled automatically from the |authtoken|.
269 :type apiuser: AuthUser
267 :type apiuser: AuthUser
270 :param repogroupid: Set the ID of repository group.
268 :param repogroupid: Set the ID of repository group.
271 :type repogroupid: str or int
269 :type repogroupid: str or int
272 :param group_name: Set the name of the |repo| group.
270 :param group_name: Set the name of the |repo| group.
273 :type group_name: str
271 :type group_name: str
274 :param description: Set a description for the group.
272 :param description: Set a description for the group.
275 :type description: str
273 :type description: str
276 :param owner: Set the |repo| group owner.
274 :param owner: Set the |repo| group owner.
277 :type owner: str
275 :type owner: str
278 :param enable_locking: Enable |repo| locking. The default is false.
276 :param enable_locking: Enable |repo| locking. The default is false.
279 :type enable_locking: bool
277 :type enable_locking: bool
280 """
278 """
281
279
282 repo_group = get_repo_group_or_error(repogroupid)
280 repo_group = get_repo_group_or_error(repogroupid)
283
281
284 if not has_superadmin_permission(apiuser):
282 if not has_superadmin_permission(apiuser):
285 validate_repo_group_permissions(
283 validate_repo_group_permissions(
286 apiuser, repogroupid, repo_group, ('group.admin',))
284 apiuser, repogroupid, repo_group, ('group.admin',))
287
285
288 updates = dict(
286 updates = dict(
289 group_name=group_name
287 group_name=group_name
290 if not isinstance(group_name, Optional) else repo_group.group_name,
288 if not isinstance(group_name, Optional) else repo_group.group_name,
291
289
292 group_description=description
290 group_description=description
293 if not isinstance(description, Optional) else repo_group.group_description,
291 if not isinstance(description, Optional) else repo_group.group_description,
294
292
295 user=owner
293 user=owner
296 if not isinstance(owner, Optional) else repo_group.user.username,
294 if not isinstance(owner, Optional) else repo_group.user.username,
297
295
298 enable_locking=enable_locking
296 enable_locking=enable_locking
299 if not isinstance(enable_locking, Optional) else repo_group.enable_locking
297 if not isinstance(enable_locking, Optional) else repo_group.enable_locking
300 )
298 )
301
299
302 schema = repo_group_schema.RepoGroupSchema().bind(
300 schema = repo_group_schema.RepoGroupSchema().bind(
303 # user caller
301 # user caller
304 user=apiuser,
302 user=apiuser,
305 old_values=repo_group.get_api_data())
303 old_values=repo_group.get_api_data())
306
304
307 try:
305 try:
308 schema_data = schema.deserialize(dict(
306 schema_data = schema.deserialize(dict(
309 repo_group_name=updates['group_name'],
307 repo_group_name=updates['group_name'],
310 repo_group_owner=updates['user'],
308 repo_group_owner=updates['user'],
311 repo_group_description=updates['group_description'],
309 repo_group_description=updates['group_description'],
312 repo_group_enable_locking=updates['enable_locking'],
310 repo_group_enable_locking=updates['enable_locking'],
313 ))
311 ))
314 except validation_schema.Invalid as err:
312 except validation_schema.Invalid as err:
315 raise JSONRPCValidationError(colander_exc=err)
313 raise JSONRPCValidationError(colander_exc=err)
316
314
317 validated_updates = dict(
315 validated_updates = dict(
318 group_name=schema_data['repo_group']['repo_group_name_without_group'],
316 group_name=schema_data['repo_group']['repo_group_name_without_group'],
319 group_parent_id=schema_data['repo_group']['repo_group_id'],
317 group_parent_id=schema_data['repo_group']['repo_group_id'],
320 user=schema_data['repo_group_owner'],
318 user=schema_data['repo_group_owner'],
321 group_description=schema_data['repo_group_description'],
319 group_description=schema_data['repo_group_description'],
322 enable_locking=schema_data['repo_group_enable_locking'],
320 enable_locking=schema_data['repo_group_enable_locking'],
323 )
321 )
324
322
325 old_data = repo_group.get_api_data()
323 old_data = repo_group.get_api_data()
326 try:
324 try:
327 RepoGroupModel().update(repo_group, validated_updates)
325 RepoGroupModel().update(repo_group, validated_updates)
328 audit_logger.store_api(
326 audit_logger.store_api(
329 'repo_group.edit', action_data={'old_data': old_data},
327 'repo_group.edit', action_data={'old_data': old_data},
330 user=apiuser)
328 user=apiuser)
331
329
332 Session().commit()
330 Session().commit()
333 return {
331 return {
334 'msg': 'updated repository group ID:%s %s' % (
332 'msg': 'updated repository group ID:{} {}'.format(
335 repo_group.group_id, repo_group.group_name),
333 repo_group.group_id, repo_group.group_name),
336 'repo_group': repo_group.get_api_data()
334 'repo_group': repo_group.get_api_data()
337 }
335 }
338 except Exception:
336 except Exception:
339 log.exception(
337 log.exception(
340 u"Exception occurred while trying update repo group %s",
338 "Exception occurred while trying update repo group %s",
341 repogroupid)
339 repogroupid)
342 raise JSONRPCError('failed to update repository group `%s`'
340 raise JSONRPCError('failed to update repository group `%s`'
343 % (repogroupid,))
341 % (repogroupid,))
344
342
345
343
346 @jsonrpc_method()
344 @jsonrpc_method()
347 def delete_repo_group(request, apiuser, repogroupid):
345 def delete_repo_group(request, apiuser, repogroupid):
348 """
346 """
349 Deletes a |repo| group.
347 Deletes a |repo| group.
350
348
351 :param apiuser: This is filled automatically from the |authtoken|.
349 :param apiuser: This is filled automatically from the |authtoken|.
352 :type apiuser: AuthUser
350 :type apiuser: AuthUser
353 :param repogroupid: Set the name or ID of repository group to be
351 :param repogroupid: Set the name or ID of repository group to be
354 deleted.
352 deleted.
355 :type repogroupid: str or int
353 :type repogroupid: str or int
356
354
357 Example output:
355 Example output:
358
356
359 .. code-block:: bash
357 .. code-block:: bash
360
358
361 id : <id_given_in_input>
359 id : <id_given_in_input>
362 result : {
360 result : {
363 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>'
361 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>'
364 'repo_group': null
362 'repo_group': null
365 }
363 }
366 error : null
364 error : null
367
365
368 Example error output:
366 Example error output:
369
367
370 .. code-block:: bash
368 .. code-block:: bash
371
369
372 id : <id_given_in_input>
370 id : <id_given_in_input>
373 result : null
371 result : null
374 error : {
372 error : {
375 "failed to delete repo group ID:<repogroupid> <repogroupname>"
373 "failed to delete repo group ID:<repogroupid> <repogroupname>"
376 }
374 }
377
375
378 """
376 """
379
377
380 repo_group = get_repo_group_or_error(repogroupid)
378 repo_group = get_repo_group_or_error(repogroupid)
381 if not has_superadmin_permission(apiuser):
379 if not has_superadmin_permission(apiuser):
382 validate_repo_group_permissions(
380 validate_repo_group_permissions(
383 apiuser, repogroupid, repo_group, ('group.admin',))
381 apiuser, repogroupid, repo_group, ('group.admin',))
384
382
385 old_data = repo_group.get_api_data()
383 old_data = repo_group.get_api_data()
386 try:
384 try:
387 RepoGroupModel().delete(repo_group)
385 RepoGroupModel().delete(repo_group)
388 audit_logger.store_api(
386 audit_logger.store_api(
389 'repo_group.delete', action_data={'old_data': old_data},
387 'repo_group.delete', action_data={'old_data': old_data},
390 user=apiuser)
388 user=apiuser)
391 Session().commit()
389 Session().commit()
392 return {
390 return {
393 'msg': 'deleted repo group ID:%s %s' %
391 'msg': 'deleted repo group ID:%s %s' %
394 (repo_group.group_id, repo_group.group_name),
392 (repo_group.group_id, repo_group.group_name),
395 'repo_group': None
393 'repo_group': None
396 }
394 }
397 except Exception:
395 except Exception:
398 log.exception("Exception occurred while trying to delete repo group")
396 log.exception("Exception occurred while trying to delete repo group")
399 raise JSONRPCError('failed to delete repo group ID:%s %s' %
397 raise JSONRPCError('failed to delete repo group ID:%s %s' %
400 (repo_group.group_id, repo_group.group_name))
398 (repo_group.group_id, repo_group.group_name))
401
399
402
400
403 @jsonrpc_method()
401 @jsonrpc_method()
404 def grant_user_permission_to_repo_group(
402 def grant_user_permission_to_repo_group(
405 request, apiuser, repogroupid, userid, perm,
403 request, apiuser, repogroupid, userid, perm,
406 apply_to_children=Optional('none')):
404 apply_to_children=Optional('none')):
407 """
405 """
408 Grant permission for a user on the given repository group, or update
406 Grant permission for a user on the given repository group, or update
409 existing permissions if found.
407 existing permissions if found.
410
408
411 This command can only be run using an |authtoken| with admin
409 This command can only be run using an |authtoken| with admin
412 permissions.
410 permissions.
413
411
414 :param apiuser: This is filled automatically from the |authtoken|.
412 :param apiuser: This is filled automatically from the |authtoken|.
415 :type apiuser: AuthUser
413 :type apiuser: AuthUser
416 :param repogroupid: Set the name or ID of repository group.
414 :param repogroupid: Set the name or ID of repository group.
417 :type repogroupid: str or int
415 :type repogroupid: str or int
418 :param userid: Set the user name.
416 :param userid: Set the user name.
419 :type userid: str
417 :type userid: str
420 :param perm: (group.(none|read|write|admin))
418 :param perm: (group.(none|read|write|admin))
421 :type perm: str
419 :type perm: str
422 :param apply_to_children: 'none', 'repos', 'groups', 'all'
420 :param apply_to_children: 'none', 'repos', 'groups', 'all'
423 :type apply_to_children: str
421 :type apply_to_children: str
424
422
425 Example output:
423 Example output:
426
424
427 .. code-block:: bash
425 .. code-block:: bash
428
426
429 id : <id_given_in_input>
427 id : <id_given_in_input>
430 result: {
428 result: {
431 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
429 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
432 "success": true
430 "success": true
433 }
431 }
434 error: null
432 error: null
435
433
436 Example error output:
434 Example error output:
437
435
438 .. code-block:: bash
436 .. code-block:: bash
439
437
440 id : <id_given_in_input>
438 id : <id_given_in_input>
441 result : null
439 result : null
442 error : {
440 error : {
443 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
441 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
444 }
442 }
445
443
446 """
444 """
447
445
448 repo_group = get_repo_group_or_error(repogroupid)
446 repo_group = get_repo_group_or_error(repogroupid)
449
447
450 if not has_superadmin_permission(apiuser):
448 if not has_superadmin_permission(apiuser):
451 validate_repo_group_permissions(
449 validate_repo_group_permissions(
452 apiuser, repogroupid, repo_group, ('group.admin',))
450 apiuser, repogroupid, repo_group, ('group.admin',))
453
451
454 user = get_user_or_error(userid)
452 user = get_user_or_error(userid)
455 perm = get_perm_or_error(perm, prefix='group.')
453 perm = get_perm_or_error(perm, prefix='group.')
456 apply_to_children = Optional.extract(apply_to_children)
454 apply_to_children = Optional.extract(apply_to_children)
457
455
458 perm_additions = [[user.user_id, perm.permission_name, "user"]]
456 perm_additions = [[user.user_id, perm.permission_name, "user"]]
459 try:
457 try:
460 changes = RepoGroupModel().update_permissions(
458 changes = RepoGroupModel().update_permissions(
461 repo_group=repo_group, perm_additions=perm_additions,
459 repo_group=repo_group, perm_additions=perm_additions,
462 recursive=apply_to_children, cur_user=apiuser)
460 recursive=apply_to_children, cur_user=apiuser)
463
461
464 action_data = {
462 action_data = {
465 'added': changes['added'],
463 'added': changes['added'],
466 'updated': changes['updated'],
464 'updated': changes['updated'],
467 'deleted': changes['deleted'],
465 'deleted': changes['deleted'],
468 }
466 }
469 audit_logger.store_api(
467 audit_logger.store_api(
470 'repo_group.edit.permissions', action_data=action_data,
468 'repo_group.edit.permissions', action_data=action_data,
471 user=apiuser)
469 user=apiuser)
472 Session().commit()
470 Session().commit()
473 PermissionModel().flush_user_permission_caches(changes)
471 PermissionModel().flush_user_permission_caches(changes)
474
472
475 return {
473 return {
476 'msg': 'Granted perm: `%s` (recursive:%s) for user: '
474 'msg': 'Granted perm: `%s` (recursive:%s) for user: '
477 '`%s` in repo group: `%s`' % (
475 '`%s` in repo group: `%s`' % (
478 perm.permission_name, apply_to_children, user.username,
476 perm.permission_name, apply_to_children, user.username,
479 repo_group.name
477 repo_group.name
480 ),
478 ),
481 'success': True
479 'success': True
482 }
480 }
483 except Exception:
481 except Exception:
484 log.exception("Exception occurred while trying to grant "
482 log.exception("Exception occurred while trying to grant "
485 "user permissions to repo group")
483 "user permissions to repo group")
486 raise JSONRPCError(
484 raise JSONRPCError(
487 'failed to edit permission for user: '
485 'failed to edit permission for user: '
488 '`%s` in repo group: `%s`' % (userid, repo_group.name))
486 '`%s` in repo group: `%s`' % (userid, repo_group.name))
489
487
490
488
491 @jsonrpc_method()
489 @jsonrpc_method()
492 def revoke_user_permission_from_repo_group(
490 def revoke_user_permission_from_repo_group(
493 request, apiuser, repogroupid, userid,
491 request, apiuser, repogroupid, userid,
494 apply_to_children=Optional('none')):
492 apply_to_children=Optional('none')):
495 """
493 """
496 Revoke permission for a user in a given repository group.
494 Revoke permission for a user in a given repository group.
497
495
498 This command can only be run using an |authtoken| with admin
496 This command can only be run using an |authtoken| with admin
499 permissions on the |repo| group.
497 permissions on the |repo| group.
500
498
501 :param apiuser: This is filled automatically from the |authtoken|.
499 :param apiuser: This is filled automatically from the |authtoken|.
502 :type apiuser: AuthUser
500 :type apiuser: AuthUser
503 :param repogroupid: Set the name or ID of the repository group.
501 :param repogroupid: Set the name or ID of the repository group.
504 :type repogroupid: str or int
502 :type repogroupid: str or int
505 :param userid: Set the user name to revoke.
503 :param userid: Set the user name to revoke.
506 :type userid: str
504 :type userid: str
507 :param apply_to_children: 'none', 'repos', 'groups', 'all'
505 :param apply_to_children: 'none', 'repos', 'groups', 'all'
508 :type apply_to_children: str
506 :type apply_to_children: str
509
507
510 Example output:
508 Example output:
511
509
512 .. code-block:: bash
510 .. code-block:: bash
513
511
514 id : <id_given_in_input>
512 id : <id_given_in_input>
515 result: {
513 result: {
516 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
514 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
517 "success": true
515 "success": true
518 }
516 }
519 error: null
517 error: null
520
518
521 Example error output:
519 Example error output:
522
520
523 .. code-block:: bash
521 .. code-block:: bash
524
522
525 id : <id_given_in_input>
523 id : <id_given_in_input>
526 result : null
524 result : null
527 error : {
525 error : {
528 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
526 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
529 }
527 }
530
528
531 """
529 """
532
530
533 repo_group = get_repo_group_or_error(repogroupid)
531 repo_group = get_repo_group_or_error(repogroupid)
534
532
535 if not has_superadmin_permission(apiuser):
533 if not has_superadmin_permission(apiuser):
536 validate_repo_group_permissions(
534 validate_repo_group_permissions(
537 apiuser, repogroupid, repo_group, ('group.admin',))
535 apiuser, repogroupid, repo_group, ('group.admin',))
538
536
539 user = get_user_or_error(userid)
537 user = get_user_or_error(userid)
540 apply_to_children = Optional.extract(apply_to_children)
538 apply_to_children = Optional.extract(apply_to_children)
541
539
542 perm_deletions = [[user.user_id, None, "user"]]
540 perm_deletions = [[user.user_id, None, "user"]]
543 try:
541 try:
544 changes = RepoGroupModel().update_permissions(
542 changes = RepoGroupModel().update_permissions(
545 repo_group=repo_group, perm_deletions=perm_deletions,
543 repo_group=repo_group, perm_deletions=perm_deletions,
546 recursive=apply_to_children, cur_user=apiuser)
544 recursive=apply_to_children, cur_user=apiuser)
547
545
548 action_data = {
546 action_data = {
549 'added': changes['added'],
547 'added': changes['added'],
550 'updated': changes['updated'],
548 'updated': changes['updated'],
551 'deleted': changes['deleted'],
549 'deleted': changes['deleted'],
552 }
550 }
553 audit_logger.store_api(
551 audit_logger.store_api(
554 'repo_group.edit.permissions', action_data=action_data,
552 'repo_group.edit.permissions', action_data=action_data,
555 user=apiuser)
553 user=apiuser)
556 Session().commit()
554 Session().commit()
557 PermissionModel().flush_user_permission_caches(changes)
555 PermissionModel().flush_user_permission_caches(changes)
558
556
559 return {
557 return {
560 'msg': 'Revoked perm (recursive:%s) for user: '
558 'msg': 'Revoked perm (recursive:%s) for user: '
561 '`%s` in repo group: `%s`' % (
559 '`%s` in repo group: `%s`' % (
562 apply_to_children, user.username, repo_group.name
560 apply_to_children, user.username, repo_group.name
563 ),
561 ),
564 'success': True
562 'success': True
565 }
563 }
566 except Exception:
564 except Exception:
567 log.exception("Exception occurred while trying revoke user "
565 log.exception("Exception occurred while trying revoke user "
568 "permission from repo group")
566 "permission from repo group")
569 raise JSONRPCError(
567 raise JSONRPCError(
570 'failed to edit permission for user: '
568 'failed to edit permission for user: '
571 '`%s` in repo group: `%s`' % (userid, repo_group.name))
569 '`%s` in repo group: `%s`' % (userid, repo_group.name))
572
570
573
571
574 @jsonrpc_method()
572 @jsonrpc_method()
575 def grant_user_group_permission_to_repo_group(
573 def grant_user_group_permission_to_repo_group(
576 request, apiuser, repogroupid, usergroupid, perm,
574 request, apiuser, repogroupid, usergroupid, perm,
577 apply_to_children=Optional('none'), ):
575 apply_to_children=Optional('none'), ):
578 """
576 """
579 Grant permission for a user group on given repository group, or update
577 Grant permission for a user group on given repository group, or update
580 existing permissions if found.
578 existing permissions if found.
581
579
582 This command can only be run using an |authtoken| with admin
580 This command can only be run using an |authtoken| with admin
583 permissions on the |repo| group.
581 permissions on the |repo| group.
584
582
585 :param apiuser: This is filled automatically from the |authtoken|.
583 :param apiuser: This is filled automatically from the |authtoken|.
586 :type apiuser: AuthUser
584 :type apiuser: AuthUser
587 :param repogroupid: Set the name or id of repository group
585 :param repogroupid: Set the name or id of repository group
588 :type repogroupid: str or int
586 :type repogroupid: str or int
589 :param usergroupid: id of usergroup
587 :param usergroupid: id of usergroup
590 :type usergroupid: str or int
588 :type usergroupid: str or int
591 :param perm: (group.(none|read|write|admin))
589 :param perm: (group.(none|read|write|admin))
592 :type perm: str
590 :type perm: str
593 :param apply_to_children: 'none', 'repos', 'groups', 'all'
591 :param apply_to_children: 'none', 'repos', 'groups', 'all'
594 :type apply_to_children: str
592 :type apply_to_children: str
595
593
596 Example output:
594 Example output:
597
595
598 .. code-block:: bash
596 .. code-block:: bash
599
597
600 id : <id_given_in_input>
598 id : <id_given_in_input>
601 result : {
599 result : {
602 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
600 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
603 "success": true
601 "success": true
604
602
605 }
603 }
606 error : null
604 error : null
607
605
608 Example error output:
606 Example error output:
609
607
610 .. code-block:: bash
608 .. code-block:: bash
611
609
612 id : <id_given_in_input>
610 id : <id_given_in_input>
613 result : null
611 result : null
614 error : {
612 error : {
615 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
613 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
616 }
614 }
617
615
618 """
616 """
619
617
620 repo_group = get_repo_group_or_error(repogroupid)
618 repo_group = get_repo_group_or_error(repogroupid)
621 perm = get_perm_or_error(perm, prefix='group.')
619 perm = get_perm_or_error(perm, prefix='group.')
622 user_group = get_user_group_or_error(usergroupid)
620 user_group = get_user_group_or_error(usergroupid)
623 if not has_superadmin_permission(apiuser):
621 if not has_superadmin_permission(apiuser):
624 validate_repo_group_permissions(
622 validate_repo_group_permissions(
625 apiuser, repogroupid, repo_group, ('group.admin',))
623 apiuser, repogroupid, repo_group, ('group.admin',))
626
624
627 # check if we have at least read permission for this user group !
625 # check if we have at least read permission for this user group !
628 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
626 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
629 if not HasUserGroupPermissionAnyApi(*_perms)(
627 if not HasUserGroupPermissionAnyApi(*_perms)(
630 user=apiuser, user_group_name=user_group.users_group_name):
628 user=apiuser, user_group_name=user_group.users_group_name):
631 raise JSONRPCError(
629 raise JSONRPCError(
632 'user group `%s` does not exist' % (usergroupid,))
630 'user group `{}` does not exist'.format(usergroupid))
633
631
634 apply_to_children = Optional.extract(apply_to_children)
632 apply_to_children = Optional.extract(apply_to_children)
635
633
636 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
634 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
637 try:
635 try:
638 changes = RepoGroupModel().update_permissions(
636 changes = RepoGroupModel().update_permissions(
639 repo_group=repo_group, perm_additions=perm_additions,
637 repo_group=repo_group, perm_additions=perm_additions,
640 recursive=apply_to_children, cur_user=apiuser)
638 recursive=apply_to_children, cur_user=apiuser)
641
639
642 action_data = {
640 action_data = {
643 'added': changes['added'],
641 'added': changes['added'],
644 'updated': changes['updated'],
642 'updated': changes['updated'],
645 'deleted': changes['deleted'],
643 'deleted': changes['deleted'],
646 }
644 }
647 audit_logger.store_api(
645 audit_logger.store_api(
648 'repo_group.edit.permissions', action_data=action_data,
646 'repo_group.edit.permissions', action_data=action_data,
649 user=apiuser)
647 user=apiuser)
650 Session().commit()
648 Session().commit()
651 PermissionModel().flush_user_permission_caches(changes)
649 PermissionModel().flush_user_permission_caches(changes)
652
650
653 return {
651 return {
654 'msg': 'Granted perm: `%s` (recursive:%s) '
652 'msg': 'Granted perm: `%s` (recursive:%s) '
655 'for user group: `%s` in repo group: `%s`' % (
653 'for user group: `%s` in repo group: `%s`' % (
656 perm.permission_name, apply_to_children,
654 perm.permission_name, apply_to_children,
657 user_group.users_group_name, repo_group.name
655 user_group.users_group_name, repo_group.name
658 ),
656 ),
659 'success': True
657 'success': True
660 }
658 }
661 except Exception:
659 except Exception:
662 log.exception("Exception occurred while trying to grant user "
660 log.exception("Exception occurred while trying to grant user "
663 "group permissions to repo group")
661 "group permissions to repo group")
664 raise JSONRPCError(
662 raise JSONRPCError(
665 'failed to edit permission for user group: `%s` in '
663 'failed to edit permission for user group: `%s` in '
666 'repo group: `%s`' % (
664 'repo group: `%s`' % (
667 usergroupid, repo_group.name
665 usergroupid, repo_group.name
668 )
666 )
669 )
667 )
670
668
671
669
672 @jsonrpc_method()
670 @jsonrpc_method()
673 def revoke_user_group_permission_from_repo_group(
671 def revoke_user_group_permission_from_repo_group(
674 request, apiuser, repogroupid, usergroupid,
672 request, apiuser, repogroupid, usergroupid,
675 apply_to_children=Optional('none')):
673 apply_to_children=Optional('none')):
676 """
674 """
677 Revoke permission for user group on given repository.
675 Revoke permission for user group on given repository.
678
676
679 This command can only be run using an |authtoken| with admin
677 This command can only be run using an |authtoken| with admin
680 permissions on the |repo| group.
678 permissions on the |repo| group.
681
679
682 :param apiuser: This is filled automatically from the |authtoken|.
680 :param apiuser: This is filled automatically from the |authtoken|.
683 :type apiuser: AuthUser
681 :type apiuser: AuthUser
684 :param repogroupid: name or id of repository group
682 :param repogroupid: name or id of repository group
685 :type repogroupid: str or int
683 :type repogroupid: str or int
686 :param usergroupid:
684 :param usergroupid:
687 :param apply_to_children: 'none', 'repos', 'groups', 'all'
685 :param apply_to_children: 'none', 'repos', 'groups', 'all'
688 :type apply_to_children: str
686 :type apply_to_children: str
689
687
690 Example output:
688 Example output:
691
689
692 .. code-block:: bash
690 .. code-block:: bash
693
691
694 id : <id_given_in_input>
692 id : <id_given_in_input>
695 result: {
693 result: {
696 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
694 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
697 "success": true
695 "success": true
698 }
696 }
699 error: null
697 error: null
700
698
701 Example error output:
699 Example error output:
702
700
703 .. code-block:: bash
701 .. code-block:: bash
704
702
705 id : <id_given_in_input>
703 id : <id_given_in_input>
706 result : null
704 result : null
707 error : {
705 error : {
708 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
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 repo_group = get_repo_group_or_error(repogroupid)
712 repo_group = get_repo_group_or_error(repogroupid)
715 user_group = get_user_group_or_error(usergroupid)
713 user_group = get_user_group_or_error(usergroupid)
716 if not has_superadmin_permission(apiuser):
714 if not has_superadmin_permission(apiuser):
717 validate_repo_group_permissions(
715 validate_repo_group_permissions(
718 apiuser, repogroupid, repo_group, ('group.admin',))
716 apiuser, repogroupid, repo_group, ('group.admin',))
719
717
720 # check if we have at least read permission for this user group !
718 # check if we have at least read permission for this user group !
721 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
719 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
722 if not HasUserGroupPermissionAnyApi(*_perms)(
720 if not HasUserGroupPermissionAnyApi(*_perms)(
723 user=apiuser, user_group_name=user_group.users_group_name):
721 user=apiuser, user_group_name=user_group.users_group_name):
724 raise JSONRPCError(
722 raise JSONRPCError(
725 'user group `%s` does not exist' % (usergroupid,))
723 'user group `{}` does not exist'.format(usergroupid))
726
724
727 apply_to_children = Optional.extract(apply_to_children)
725 apply_to_children = Optional.extract(apply_to_children)
728
726
729 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
727 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
730 try:
728 try:
731 changes = RepoGroupModel().update_permissions(
729 changes = RepoGroupModel().update_permissions(
732 repo_group=repo_group, perm_deletions=perm_deletions,
730 repo_group=repo_group, perm_deletions=perm_deletions,
733 recursive=apply_to_children, cur_user=apiuser)
731 recursive=apply_to_children, cur_user=apiuser)
734
732
735 action_data = {
733 action_data = {
736 'added': changes['added'],
734 'added': changes['added'],
737 'updated': changes['updated'],
735 'updated': changes['updated'],
738 'deleted': changes['deleted'],
736 'deleted': changes['deleted'],
739 }
737 }
740 audit_logger.store_api(
738 audit_logger.store_api(
741 'repo_group.edit.permissions', action_data=action_data,
739 'repo_group.edit.permissions', action_data=action_data,
742 user=apiuser)
740 user=apiuser)
743 Session().commit()
741 Session().commit()
744 PermissionModel().flush_user_permission_caches(changes)
742 PermissionModel().flush_user_permission_caches(changes)
745
743
746 return {
744 return {
747 'msg': 'Revoked perm (recursive:%s) for user group: '
745 'msg': 'Revoked perm (recursive:%s) for user group: '
748 '`%s` in repo group: `%s`' % (
746 '`%s` in repo group: `%s`' % (
749 apply_to_children, user_group.users_group_name,
747 apply_to_children, user_group.users_group_name,
750 repo_group.name
748 repo_group.name
751 ),
749 ),
752 'success': True
750 'success': True
753 }
751 }
754 except Exception:
752 except Exception:
755 log.exception("Exception occurred while trying revoke user group "
753 log.exception("Exception occurred while trying revoke user group "
756 "permissions from repo group")
754 "permissions from repo group")
757 raise JSONRPCError(
755 raise JSONRPCError(
758 'failed to edit permission for user group: '
756 'failed to edit permission for user group: '
759 '`%s` in repo group: `%s`' % (
757 '`%s` in repo group: `%s`' % (
760 user_group.users_group_name, repo_group.name
758 user_group.users_group_name, repo_group.name
761 )
759 )
762 )
760 )
@@ -1,152 +1,150 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 import logging
20 import logging
23
21
24 from rhodecode.api import jsonrpc_method
22 from rhodecode.api import jsonrpc_method
25 from rhodecode.api.exc import JSONRPCValidationError, JSONRPCForbidden
23 from rhodecode.api.exc import JSONRPCValidationError, JSONRPCForbidden
26 from rhodecode.api.utils import Optional, has_superadmin_permission
24 from rhodecode.api.utils import Optional, has_superadmin_permission
27 from rhodecode.lib.index import searcher_from_config
25 from rhodecode.lib.index import searcher_from_config
28 from rhodecode.lib.user_log_filter import user_log_filter
26 from rhodecode.lib.user_log_filter import user_log_filter
29 from rhodecode.model import validation_schema
27 from rhodecode.model import validation_schema
30 from rhodecode.model.db import joinedload, UserLog
28 from rhodecode.model.db import joinedload, UserLog
31 from rhodecode.model.validation_schema.schemas import search_schema
29 from rhodecode.model.validation_schema.schemas import search_schema
32
30
33 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
34
32
35
33
36 @jsonrpc_method()
34 @jsonrpc_method()
37 def search(request, apiuser, search_query, search_type, page_limit=Optional(10),
35 def search(request, apiuser, search_query, search_type, page_limit=Optional(10),
38 page=Optional(1), search_sort=Optional('desc:date'),
36 page=Optional(1), search_sort=Optional('desc:date'),
39 repo_name=Optional(None), repo_group_name=Optional(None)):
37 repo_name=Optional(None), repo_group_name=Optional(None)):
40 """
38 """
41 Fetch Full Text Search results using API.
39 Fetch Full Text Search results using API.
42
40
43 :param apiuser: This is filled automatically from the |authtoken|.
41 :param apiuser: This is filled automatically from the |authtoken|.
44 :type apiuser: AuthUser
42 :type apiuser: AuthUser
45 :param search_query: Search query.
43 :param search_query: Search query.
46 :type search_query: str
44 :type search_query: str
47 :param search_type: Search type. The following are valid options:
45 :param search_type: Search type. The following are valid options:
48 * commit
46 * commit
49 * content
47 * content
50 * path
48 * path
51 :type search_type: str
49 :type search_type: str
52 :param page_limit: Page item limit, from 1 to 500. Default 10 items.
50 :param page_limit: Page item limit, from 1 to 500. Default 10 items.
53 :type page_limit: Optional(int)
51 :type page_limit: Optional(int)
54 :param page: Page number. Default first page.
52 :param page: Page number. Default first page.
55 :type page: Optional(int)
53 :type page: Optional(int)
56 :param search_sort: Search sort order.Must start with asc: or desc: Default desc:date.
54 :param search_sort: Search sort order.Must start with asc: or desc: Default desc:date.
57 The following are valid options:
55 The following are valid options:
58 * asc|desc:message.raw
56 * asc|desc:message.raw
59 * asc|desc:date
57 * asc|desc:date
60 * asc|desc:author.email.raw
58 * asc|desc:author.email.raw
61 * asc|desc:message.raw
59 * asc|desc:message.raw
62 * newfirst (old legacy equal to desc:date)
60 * newfirst (old legacy equal to desc:date)
63 * oldfirst (old legacy equal to asc:date)
61 * oldfirst (old legacy equal to asc:date)
64
62
65 :type search_sort: Optional(str)
63 :type search_sort: Optional(str)
66 :param repo_name: Filter by one repo. Default is all.
64 :param repo_name: Filter by one repo. Default is all.
67 :type repo_name: Optional(str)
65 :type repo_name: Optional(str)
68 :param repo_group_name: Filter by one repo group. Default is all.
66 :param repo_group_name: Filter by one repo group. Default is all.
69 :type repo_group_name: Optional(str)
67 :type repo_group_name: Optional(str)
70 """
68 """
71
69
72 data = {'execution_time': ''}
70 data = {'execution_time': ''}
73 repo_name = Optional.extract(repo_name)
71 repo_name = Optional.extract(repo_name)
74 repo_group_name = Optional.extract(repo_group_name)
72 repo_group_name = Optional.extract(repo_group_name)
75
73
76 schema = search_schema.SearchParamsSchema()
74 schema = search_schema.SearchParamsSchema()
77
75
78 try:
76 try:
79 search_params = schema.deserialize(
77 search_params = schema.deserialize(
80 dict(search_query=search_query,
78 dict(search_query=search_query,
81 search_type=search_type,
79 search_type=search_type,
82 search_sort=Optional.extract(search_sort),
80 search_sort=Optional.extract(search_sort),
83 page_limit=Optional.extract(page_limit),
81 page_limit=Optional.extract(page_limit),
84 requested_page=Optional.extract(page))
82 requested_page=Optional.extract(page))
85 )
83 )
86 except validation_schema.Invalid as err:
84 except validation_schema.Invalid as err:
87 raise JSONRPCValidationError(colander_exc=err)
85 raise JSONRPCValidationError(colander_exc=err)
88
86
89 search_query = search_params.get('search_query')
87 search_query = search_params.get('search_query')
90 search_type = search_params.get('search_type')
88 search_type = search_params.get('search_type')
91 search_sort = search_params.get('search_sort')
89 search_sort = search_params.get('search_sort')
92
90
93 if search_params.get('search_query'):
91 if search_params.get('search_query'):
94 page_limit = search_params['page_limit']
92 page_limit = search_params['page_limit']
95 requested_page = search_params['requested_page']
93 requested_page = search_params['requested_page']
96
94
97 searcher = searcher_from_config(request.registry.settings)
95 searcher = searcher_from_config(request.registry.settings)
98
96
99 try:
97 try:
100 search_result = searcher.search(
98 search_result = searcher.search(
101 search_query, search_type, apiuser, repo_name, repo_group_name,
99 search_query, search_type, apiuser, repo_name, repo_group_name,
102 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
100 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
103
101
104 data.update(dict(
102 data.update(dict(
105 results=list(search_result['results']), page=requested_page,
103 results=list(search_result['results']), page=requested_page,
106 item_count=search_result['count'],
104 item_count=search_result['count'],
107 items_per_page=page_limit))
105 items_per_page=page_limit))
108 finally:
106 finally:
109 searcher.cleanup()
107 searcher.cleanup()
110
108
111 if not search_result['error']:
109 if not search_result['error']:
112 data['execution_time'] = '%s results (%.4f seconds)' % (
110 data['execution_time'] = '{} results ({:.4f} seconds)'.format(
113 search_result['count'],
111 search_result['count'],
114 search_result['runtime'])
112 search_result['runtime'])
115 else:
113 else:
116 node = schema['search_query']
114 node = schema['search_query']
117 raise JSONRPCValidationError(
115 raise JSONRPCValidationError(
118 colander_exc=validation_schema.Invalid(node, search_result['error']))
116 colander_exc=validation_schema.Invalid(node, search_result['error']))
119
117
120 return data
118 return data
121
119
122
120
123 @jsonrpc_method()
121 @jsonrpc_method()
124 def get_audit_logs(request, apiuser, query):
122 def get_audit_logs(request, apiuser, query):
125 """
123 """
126 return full audit logs based on the query.
124 return full audit logs based on the query.
127
125
128 Please see `example query in admin > settings > audit logs` for examples
126 Please see `example query in admin > settings > audit logs` for examples
129
127
130 :param apiuser: This is filled automatically from the |authtoken|.
128 :param apiuser: This is filled automatically from the |authtoken|.
131 :type apiuser: AuthUser
129 :type apiuser: AuthUser
132 :param query: filter query, example: action:repo.artifact.add date:[20200401 TO 20200601]"
130 :param query: filter query, example: action:repo.artifact.add date:[20200401 TO 20200601]"
133 :type query: str
131 :type query: str
134 """
132 """
135
133
136 if not has_superadmin_permission(apiuser):
134 if not has_superadmin_permission(apiuser):
137 raise JSONRPCForbidden()
135 raise JSONRPCForbidden()
138
136
139 filter_term = query
137 filter_term = query
140 ret = []
138 ret = []
141
139
142 # show all user actions
140 # show all user actions
143 user_log = UserLog.query() \
141 user_log = UserLog.query() \
144 .options(joinedload(UserLog.user)) \
142 .options(joinedload(UserLog.user)) \
145 .options(joinedload(UserLog.repository)) \
143 .options(joinedload(UserLog.repository)) \
146 .order_by(UserLog.action_date.desc())
144 .order_by(UserLog.action_date.desc())
147
145
148 audit_log = user_log_filter(user_log, filter_term)
146 audit_log = user_log_filter(user_log, filter_term)
149
147
150 for entry in audit_log:
148 for entry in audit_log:
151 ret.append(entry)
149 ret.append(entry)
152 return ret
150 return ret
@@ -1,420 +1,418 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import itertools
20 import itertools
23 import base64
21 import base64
24
22
25 from rhodecode.api import (
23 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
27
25
28 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
29 Optional, OAttr, has_superadmin_permission, get_user_or_error)
27 Optional, OAttr, has_superadmin_permission, get_user_or_error)
30 from rhodecode.lib.utils import repo2db_mapper
28 from rhodecode.lib.utils import repo2db_mapper
31 from rhodecode.lib import system_info
29 from rhodecode.lib import system_info
32 from rhodecode.lib import user_sessions
30 from rhodecode.lib import user_sessions
33 from rhodecode.lib import exc_tracking
31 from rhodecode.lib import exc_tracking
34 from rhodecode.lib.ext_json import json
32 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.utils2 import safe_int
33 from rhodecode.lib.utils2 import safe_int
36 from rhodecode.model.db import UserIpMap
34 from rhodecode.model.db import UserIpMap
37 from rhodecode.model.scm import ScmModel
35 from rhodecode.model.scm import ScmModel
38 from rhodecode.model.settings import VcsSettingsModel
36 from rhodecode.model.settings import VcsSettingsModel
39 from rhodecode.apps.file_store import utils
37 from rhodecode.apps.file_store import utils
40 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
38 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
41 FileOverSizeException
39 FileOverSizeException
42
40
43 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
44
42
45
43
46 @jsonrpc_method()
44 @jsonrpc_method()
47 def get_server_info(request, apiuser):
45 def get_server_info(request, apiuser):
48 """
46 """
49 Returns the |RCE| server information.
47 Returns the |RCE| server information.
50
48
51 This includes the running version of |RCE| and all installed
49 This includes the running version of |RCE| and all installed
52 packages. This command takes the following options:
50 packages. This command takes the following options:
53
51
54 :param apiuser: This is filled automatically from the |authtoken|.
52 :param apiuser: This is filled automatically from the |authtoken|.
55 :type apiuser: AuthUser
53 :type apiuser: AuthUser
56
54
57 Example output:
55 Example output:
58
56
59 .. code-block:: bash
57 .. code-block:: bash
60
58
61 id : <id_given_in_input>
59 id : <id_given_in_input>
62 result : {
60 result : {
63 'modules': [<module name>,...]
61 'modules': [<module name>,...]
64 'py_version': <python version>,
62 'py_version': <python version>,
65 'platform': <platform type>,
63 'platform': <platform type>,
66 'rhodecode_version': <rhodecode version>
64 'rhodecode_version': <rhodecode version>
67 }
65 }
68 error : null
66 error : null
69 """
67 """
70
68
71 if not has_superadmin_permission(apiuser):
69 if not has_superadmin_permission(apiuser):
72 raise JSONRPCForbidden()
70 raise JSONRPCForbidden()
73
71
74 server_info = ScmModel().get_server_info(request.environ)
72 server_info = ScmModel().get_server_info(request.environ)
75 # rhodecode-index requires those
73 # rhodecode-index requires those
76
74
77 server_info['index_storage'] = server_info['search']['value']['location']
75 server_info['index_storage'] = server_info['search']['value']['location']
78 server_info['storage'] = server_info['storage']['value']['path']
76 server_info['storage'] = server_info['storage']['value']['path']
79
77
80 return server_info
78 return server_info
81
79
82
80
83 @jsonrpc_method()
81 @jsonrpc_method()
84 def get_repo_store(request, apiuser):
82 def get_repo_store(request, apiuser):
85 """
83 """
86 Returns the |RCE| repository storage information.
84 Returns the |RCE| repository storage information.
87
85
88 :param apiuser: This is filled automatically from the |authtoken|.
86 :param apiuser: This is filled automatically from the |authtoken|.
89 :type apiuser: AuthUser
87 :type apiuser: AuthUser
90
88
91 Example output:
89 Example output:
92
90
93 .. code-block:: bash
91 .. code-block:: bash
94
92
95 id : <id_given_in_input>
93 id : <id_given_in_input>
96 result : {
94 result : {
97 'modules': [<module name>,...]
95 'modules': [<module name>,...]
98 'py_version': <python version>,
96 'py_version': <python version>,
99 'platform': <platform type>,
97 'platform': <platform type>,
100 'rhodecode_version': <rhodecode version>
98 'rhodecode_version': <rhodecode version>
101 }
99 }
102 error : null
100 error : null
103 """
101 """
104
102
105 if not has_superadmin_permission(apiuser):
103 if not has_superadmin_permission(apiuser):
106 raise JSONRPCForbidden()
104 raise JSONRPCForbidden()
107
105
108 path = VcsSettingsModel().get_repos_location()
106 path = VcsSettingsModel().get_repos_location()
109 return {"path": path}
107 return {"path": path}
110
108
111
109
112 @jsonrpc_method()
110 @jsonrpc_method()
113 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
111 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
114 """
112 """
115 Displays the IP Address as seen from the |RCE| server.
113 Displays the IP Address as seen from the |RCE| server.
116
114
117 * This command displays the IP Address, as well as all the defined IP
115 * This command displays the IP Address, as well as all the defined IP
118 addresses for the specified user. If the ``userid`` is not set, the
116 addresses for the specified user. If the ``userid`` is not set, the
119 data returned is for the user calling the method.
117 data returned is for the user calling the method.
120
118
121 This command can only be run using an |authtoken| with admin rights to
119 This command can only be run using an |authtoken| with admin rights to
122 the specified repository.
120 the specified repository.
123
121
124 This command takes the following options:
122 This command takes the following options:
125
123
126 :param apiuser: This is filled automatically from |authtoken|.
124 :param apiuser: This is filled automatically from |authtoken|.
127 :type apiuser: AuthUser
125 :type apiuser: AuthUser
128 :param userid: Sets the userid for which associated IP Address data
126 :param userid: Sets the userid for which associated IP Address data
129 is returned.
127 is returned.
130 :type userid: Optional(str or int)
128 :type userid: Optional(str or int)
131
129
132 Example output:
130 Example output:
133
131
134 .. code-block:: bash
132 .. code-block:: bash
135
133
136 id : <id_given_in_input>
134 id : <id_given_in_input>
137 result : {
135 result : {
138 "server_ip_addr": "<ip_from_clien>",
136 "server_ip_addr": "<ip_from_clien>",
139 "user_ips": [
137 "user_ips": [
140 {
138 {
141 "ip_addr": "<ip_with_mask>",
139 "ip_addr": "<ip_with_mask>",
142 "ip_range": ["<start_ip>", "<end_ip>"],
140 "ip_range": ["<start_ip>", "<end_ip>"],
143 },
141 },
144 ...
142 ...
145 ]
143 ]
146 }
144 }
147
145
148 """
146 """
149 if not has_superadmin_permission(apiuser):
147 if not has_superadmin_permission(apiuser):
150 raise JSONRPCForbidden()
148 raise JSONRPCForbidden()
151
149
152 userid = Optional.extract(userid, evaluate_locals=locals())
150 userid = Optional.extract(userid, evaluate_locals=locals())
153 userid = getattr(userid, 'user_id', userid)
151 userid = getattr(userid, 'user_id', userid)
154
152
155 user = get_user_or_error(userid)
153 user = get_user_or_error(userid)
156 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
154 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
157 return {
155 return {
158 'server_ip_addr': request.rpc_ip_addr,
156 'server_ip_addr': request.rpc_ip_addr,
159 'user_ips': ips
157 'user_ips': ips
160 }
158 }
161
159
162
160
163 @jsonrpc_method()
161 @jsonrpc_method()
164 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
162 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
165 """
163 """
166 Triggers a rescan of the specified repositories.
164 Triggers a rescan of the specified repositories.
167
165
168 * If the ``remove_obsolete`` option is set, it also deletes repositories
166 * If the ``remove_obsolete`` option is set, it also deletes repositories
169 that are found in the database but not on the file system, so called
167 that are found in the database but not on the file system, so called
170 "clean zombies".
168 "clean zombies".
171
169
172 This command can only be run using an |authtoken| with admin rights to
170 This command can only be run using an |authtoken| with admin rights to
173 the specified repository.
171 the specified repository.
174
172
175 This command takes the following options:
173 This command takes the following options:
176
174
177 :param apiuser: This is filled automatically from the |authtoken|.
175 :param apiuser: This is filled automatically from the |authtoken|.
178 :type apiuser: AuthUser
176 :type apiuser: AuthUser
179 :param remove_obsolete: Deletes repositories from the database that
177 :param remove_obsolete: Deletes repositories from the database that
180 are not found on the filesystem.
178 are not found on the filesystem.
181 :type remove_obsolete: Optional(``True`` | ``False``)
179 :type remove_obsolete: Optional(``True`` | ``False``)
182
180
183 Example output:
181 Example output:
184
182
185 .. code-block:: bash
183 .. code-block:: bash
186
184
187 id : <id_given_in_input>
185 id : <id_given_in_input>
188 result : {
186 result : {
189 'added': [<added repository name>,...]
187 'added': [<added repository name>,...]
190 'removed': [<removed repository name>,...]
188 'removed': [<removed repository name>,...]
191 }
189 }
192 error : null
190 error : null
193
191
194 Example error output:
192 Example error output:
195
193
196 .. code-block:: bash
194 .. code-block:: bash
197
195
198 id : <id_given_in_input>
196 id : <id_given_in_input>
199 result : null
197 result : null
200 error : {
198 error : {
201 'Error occurred during rescan repositories action'
199 'Error occurred during rescan repositories action'
202 }
200 }
203
201
204 """
202 """
205 if not has_superadmin_permission(apiuser):
203 if not has_superadmin_permission(apiuser):
206 raise JSONRPCForbidden()
204 raise JSONRPCForbidden()
207
205
208 try:
206 try:
209 rm_obsolete = Optional.extract(remove_obsolete)
207 rm_obsolete = Optional.extract(remove_obsolete)
210 added, removed = repo2db_mapper(ScmModel().repo_scan(),
208 added, removed = repo2db_mapper(ScmModel().repo_scan(),
211 remove_obsolete=rm_obsolete)
209 remove_obsolete=rm_obsolete)
212 return {'added': added, 'removed': removed}
210 return {'added': added, 'removed': removed}
213 except Exception:
211 except Exception:
214 log.exception('Failed to run repo rescann')
212 log.exception('Failed to run repo rescann')
215 raise JSONRPCError(
213 raise JSONRPCError(
216 'Error occurred during rescan repositories action'
214 'Error occurred during rescan repositories action'
217 )
215 )
218
216
219
217
220 @jsonrpc_method()
218 @jsonrpc_method()
221 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
219 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
222 """
220 """
223 Triggers a session cleanup action.
221 Triggers a session cleanup action.
224
222
225 If the ``older_then`` option is set, only sessions that hasn't been
223 If the ``older_then`` option is set, only sessions that hasn't been
226 accessed in the given number of days will be removed.
224 accessed in the given number of days will be removed.
227
225
228 This command can only be run using an |authtoken| with admin rights to
226 This command can only be run using an |authtoken| with admin rights to
229 the specified repository.
227 the specified repository.
230
228
231 This command takes the following options:
229 This command takes the following options:
232
230
233 :param apiuser: This is filled automatically from the |authtoken|.
231 :param apiuser: This is filled automatically from the |authtoken|.
234 :type apiuser: AuthUser
232 :type apiuser: AuthUser
235 :param older_then: Deletes session that hasn't been accessed
233 :param older_then: Deletes session that hasn't been accessed
236 in given number of days.
234 in given number of days.
237 :type older_then: Optional(int)
235 :type older_then: Optional(int)
238
236
239 Example output:
237 Example output:
240
238
241 .. code-block:: bash
239 .. code-block:: bash
242
240
243 id : <id_given_in_input>
241 id : <id_given_in_input>
244 result: {
242 result: {
245 "backend": "<type of backend>",
243 "backend": "<type of backend>",
246 "sessions_removed": <number_of_removed_sessions>
244 "sessions_removed": <number_of_removed_sessions>
247 }
245 }
248 error : null
246 error : null
249
247
250 Example error output:
248 Example error output:
251
249
252 .. code-block:: bash
250 .. code-block:: bash
253
251
254 id : <id_given_in_input>
252 id : <id_given_in_input>
255 result : null
253 result : null
256 error : {
254 error : {
257 'Error occurred during session cleanup'
255 'Error occurred during session cleanup'
258 }
256 }
259
257
260 """
258 """
261 if not has_superadmin_permission(apiuser):
259 if not has_superadmin_permission(apiuser):
262 raise JSONRPCForbidden()
260 raise JSONRPCForbidden()
263
261
264 older_then = safe_int(Optional.extract(older_then)) or 60
262 older_then = safe_int(Optional.extract(older_then)) or 60
265 older_than_seconds = 60 * 60 * 24 * older_then
263 older_than_seconds = 60 * 60 * 24 * older_then
266
264
267 config = system_info.rhodecode_config().get_value()['value']['config']
265 config = system_info.rhodecode_config().get_value()['value']['config']
268 session_model = user_sessions.get_session_handler(
266 session_model = user_sessions.get_session_handler(
269 config.get('beaker.session.type', 'memory'))(config)
267 config.get('beaker.session.type', 'memory'))(config)
270
268
271 backend = session_model.SESSION_TYPE
269 backend = session_model.SESSION_TYPE
272 try:
270 try:
273 cleaned = session_model.clean_sessions(
271 cleaned = session_model.clean_sessions(
274 older_than_seconds=older_than_seconds)
272 older_than_seconds=older_than_seconds)
275 return {'sessions_removed': cleaned, 'backend': backend}
273 return {'sessions_removed': cleaned, 'backend': backend}
276 except user_sessions.CleanupCommand as msg:
274 except user_sessions.CleanupCommand as msg:
277 return {'cleanup_command': msg.message, 'backend': backend}
275 return {'cleanup_command': msg.message, 'backend': backend}
278 except Exception as e:
276 except Exception as e:
279 log.exception('Failed session cleanup')
277 log.exception('Failed session cleanup')
280 raise JSONRPCError(
278 raise JSONRPCError(
281 'Error occurred during session cleanup'
279 'Error occurred during session cleanup'
282 )
280 )
283
281
284
282
285 @jsonrpc_method()
283 @jsonrpc_method()
286 def get_method(request, apiuser, pattern=Optional('*')):
284 def get_method(request, apiuser, pattern=Optional('*')):
287 """
285 """
288 Returns list of all available API methods. By default match pattern
286 Returns list of all available API methods. By default match pattern
289 os "*" but any other pattern can be specified. eg *comment* will return
287 os "*" but any other pattern can be specified. eg *comment* will return
290 all methods with comment inside them. If just single method is matched
288 all methods with comment inside them. If just single method is matched
291 returned data will also include method specification
289 returned data will also include method specification
292
290
293 This command can only be run using an |authtoken| with admin rights to
291 This command can only be run using an |authtoken| with admin rights to
294 the specified repository.
292 the specified repository.
295
293
296 This command takes the following options:
294 This command takes the following options:
297
295
298 :param apiuser: This is filled automatically from the |authtoken|.
296 :param apiuser: This is filled automatically from the |authtoken|.
299 :type apiuser: AuthUser
297 :type apiuser: AuthUser
300 :param pattern: pattern to match method names against
298 :param pattern: pattern to match method names against
301 :type pattern: Optional("*")
299 :type pattern: Optional("*")
302
300
303 Example output:
301 Example output:
304
302
305 .. code-block:: bash
303 .. code-block:: bash
306
304
307 id : <id_given_in_input>
305 id : <id_given_in_input>
308 "result": [
306 "result": [
309 "changeset_comment",
307 "changeset_comment",
310 "comment_pull_request",
308 "comment_pull_request",
311 "comment_commit"
309 "comment_commit"
312 ]
310 ]
313 error : null
311 error : null
314
312
315 .. code-block:: bash
313 .. code-block:: bash
316
314
317 id : <id_given_in_input>
315 id : <id_given_in_input>
318 "result": [
316 "result": [
319 "comment_commit",
317 "comment_commit",
320 {
318 {
321 "apiuser": "<RequiredType>",
319 "apiuser": "<RequiredType>",
322 "comment_type": "<Optional:u'note'>",
320 "comment_type": "<Optional:u'note'>",
323 "commit_id": "<RequiredType>",
321 "commit_id": "<RequiredType>",
324 "message": "<RequiredType>",
322 "message": "<RequiredType>",
325 "repoid": "<RequiredType>",
323 "repoid": "<RequiredType>",
326 "request": "<RequiredType>",
324 "request": "<RequiredType>",
327 "resolves_comment_id": "<Optional:None>",
325 "resolves_comment_id": "<Optional:None>",
328 "status": "<Optional:None>",
326 "status": "<Optional:None>",
329 "userid": "<Optional:<OptionalAttr:apiuser>>"
327 "userid": "<Optional:<OptionalAttr:apiuser>>"
330 }
328 }
331 ]
329 ]
332 error : null
330 error : null
333 """
331 """
334 from rhodecode.config.patches import inspect_getargspec
332 from rhodecode.config.patches import inspect_getargspec
335 inspect = inspect_getargspec()
333 inspect = inspect_getargspec()
336
334
337 if not has_superadmin_permission(apiuser):
335 if not has_superadmin_permission(apiuser):
338 raise JSONRPCForbidden()
336 raise JSONRPCForbidden()
339
337
340 pattern = Optional.extract(pattern)
338 pattern = Optional.extract(pattern)
341
339
342 matches = find_methods(request.registry.jsonrpc_methods, pattern)
340 matches = find_methods(request.registry.jsonrpc_methods, pattern)
343
341
344 args_desc = []
342 args_desc = []
345 matches_keys = list(matches.keys())
343 matches_keys = list(matches.keys())
346 if len(matches_keys) == 1:
344 if len(matches_keys) == 1:
347 func = matches[matches_keys[0]]
345 func = matches[matches_keys[0]]
348
346
349 argspec = inspect.getargspec(func)
347 argspec = inspect.getargspec(func)
350 arglist = argspec[0]
348 arglist = argspec[0]
351 defaults = list(map(repr, argspec[3] or []))
349 defaults = list(map(repr, argspec[3] or []))
352
350
353 default_empty = '<RequiredType>'
351 default_empty = '<RequiredType>'
354
352
355 # kw arguments required by this method
353 # kw arguments required by this method
356 func_kwargs = dict(itertools.zip_longest(
354 func_kwargs = dict(itertools.zip_longest(
357 reversed(arglist), reversed(defaults), fillvalue=default_empty))
355 reversed(arglist), reversed(defaults), fillvalue=default_empty))
358 args_desc.append(func_kwargs)
356 args_desc.append(func_kwargs)
359
357
360 return matches_keys + args_desc
358 return matches_keys + args_desc
361
359
362
360
363 @jsonrpc_method()
361 @jsonrpc_method()
364 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
362 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
365 """
363 """
366 Stores sent exception inside the built-in exception tracker in |RCE| server.
364 Stores sent exception inside the built-in exception tracker in |RCE| server.
367
365
368 This command can only be run using an |authtoken| with admin rights to
366 This command can only be run using an |authtoken| with admin rights to
369 the specified repository.
367 the specified repository.
370
368
371 This command takes the following options:
369 This command takes the following options:
372
370
373 :param apiuser: This is filled automatically from the |authtoken|.
371 :param apiuser: This is filled automatically from the |authtoken|.
374 :type apiuser: AuthUser
372 :type apiuser: AuthUser
375
373
376 :param exc_data_json: JSON data with exception e.g
374 :param exc_data_json: JSON data with exception e.g
377 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
375 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
378 :type exc_data_json: JSON data
376 :type exc_data_json: JSON data
379
377
380 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
378 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
381 :type prefix: Optional("rhodecode")
379 :type prefix: Optional("rhodecode")
382
380
383 Example output:
381 Example output:
384
382
385 .. code-block:: bash
383 .. code-block:: bash
386
384
387 id : <id_given_in_input>
385 id : <id_given_in_input>
388 "result": {
386 "result": {
389 "exc_id": 139718459226384,
387 "exc_id": 139718459226384,
390 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
388 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
391 }
389 }
392 error : null
390 error : null
393 """
391 """
394 if not has_superadmin_permission(apiuser):
392 if not has_superadmin_permission(apiuser):
395 raise JSONRPCForbidden()
393 raise JSONRPCForbidden()
396
394
397 prefix = Optional.extract(prefix)
395 prefix = Optional.extract(prefix)
398 exc_id = exc_tracking.generate_id()
396 exc_id = exc_tracking.generate_id()
399
397
400 try:
398 try:
401 exc_data = json.loads(exc_data_json)
399 exc_data = json.loads(exc_data_json)
402 except Exception:
400 except Exception:
403 log.error('Failed to parse JSON: %r', exc_data_json)
401 log.error('Failed to parse JSON: %r', exc_data_json)
404 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
402 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
405 'Please make sure it contains a valid JSON.')
403 'Please make sure it contains a valid JSON.')
406
404
407 try:
405 try:
408 exc_traceback = exc_data['exc_traceback']
406 exc_traceback = exc_data['exc_traceback']
409 exc_type_name = exc_data['exc_type_name']
407 exc_type_name = exc_data['exc_type_name']
410 except KeyError as err:
408 except KeyError as err:
411 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
409 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
412 'in exc_data_json field. Missing: {}'.format(err))
410 'in exc_data_json field. Missing: {}'.format(err))
413
411
414 exc_tracking._store_exception(
412 exc_tracking._store_exception(
415 exc_id=exc_id, exc_traceback=exc_traceback,
413 exc_id=exc_id, exc_traceback=exc_traceback,
416 exc_type_name=exc_type_name, prefix=prefix)
414 exc_type_name=exc_type_name, prefix=prefix)
417
415
418 exc_url = request.route_url(
416 exc_url = request.route_url(
419 'admin_settings_exception_tracker_show', exception_id=exc_id)
417 'admin_settings_exception_tracker_show', exception_id=exc_id)
420 return {'exc_id': exc_id, 'exc_url': exc_url}
418 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,93 +1,91 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 import datetime
20 import datetime
23 import decimal
21 import decimal
24 import logging
22 import logging
25 import time
23 import time
26
24
27 from rhodecode.api import jsonrpc_method, jsonrpc_deprecated_method, JSONRPCError, JSONRPCForbidden
25 from rhodecode.api import jsonrpc_method, jsonrpc_deprecated_method, JSONRPCError, JSONRPCForbidden
28
26
29 from rhodecode.api.utils import Optional, OAttr
27 from rhodecode.api.utils import Optional, OAttr
30
28
31 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
32
30
33
31
34 @jsonrpc_method()
32 @jsonrpc_method()
35 def test(request, apiuser, args):
33 def test(request, apiuser, args):
36 return args
34 return args
37
35
38
36
39 @jsonrpc_method()
37 @jsonrpc_method()
40 def test_ok(request, apiuser):
38 def test_ok(request, apiuser):
41 return {
39 return {
42 'who': f'hello {apiuser}',
40 'who': f'hello {apiuser}',
43 'obj': {
41 'obj': {
44 'time': time.time(),
42 'time': time.time(),
45 'dt': datetime.datetime.now(),
43 'dt': datetime.datetime.now(),
46 'decimal': decimal.Decimal('0.123')
44 'decimal': decimal.Decimal('0.123')
47 }
45 }
48 }
46 }
49
47
50
48
51 @jsonrpc_method()
49 @jsonrpc_method()
52 def test_error(request, apiuser):
50 def test_error(request, apiuser):
53 raise JSONRPCError('error happened')
51 raise JSONRPCError('error happened')
54
52
55
53
56 @jsonrpc_method()
54 @jsonrpc_method()
57 def test_exception(request, apiuser):
55 def test_exception(request, apiuser):
58 raise Exception('something unhandled')
56 raise Exception('something unhandled')
59
57
60
58
61 @jsonrpc_method()
59 @jsonrpc_method()
62 def test_params(request, apiuser, params):
60 def test_params(request, apiuser, params):
63 return {
61 return {
64 'who': f'hello {apiuser}',
62 'who': f'hello {apiuser}',
65 'params': params
63 'params': params
66 }
64 }
67
65
68
66
69 @jsonrpc_method()
67 @jsonrpc_method()
70 def test_params_opt(
68 def test_params_opt(
71 request, apiuser, params, opt1=False, opt2=Optional(True),
69 request, apiuser, params, opt1=False, opt2=Optional(True),
72 opt3=Optional(OAttr('apiuser'))):
70 opt3=Optional(OAttr('apiuser'))):
73 opt2 = Optional.extract(opt2)
71 opt2 = Optional.extract(opt2)
74 opt3 = Optional.extract(opt3, evaluate_locals=locals())
72 opt3 = Optional.extract(opt3, evaluate_locals=locals())
75 return {
73 return {
76 'who': f'hello {apiuser}',
74 'who': f'hello {apiuser}',
77 'params': params,
75 'params': params,
78 'opts': [
76 'opts': [
79 opt1, opt2, opt3
77 opt1, opt2, opt3
80 ]
78 ]
81 }
79 }
82
80
83
81
84 @jsonrpc_method()
82 @jsonrpc_method()
85 @jsonrpc_deprecated_method(
83 @jsonrpc_deprecated_method(
86 use_method='test_ok', deprecated_at_version='4.0.0')
84 use_method='test_ok', deprecated_at_version='4.0.0')
87 def test_deprecated_method(request, apiuser):
85 def test_deprecated_method(request, apiuser):
88 return 'value'
86 return 'value'
89
87
90
88
91 @jsonrpc_method()
89 @jsonrpc_method()
92 def test_forbidden_method(request, apiuser):
90 def test_forbidden_method(request, apiuser):
93 raise JSONRPCForbidden()
91 raise JSONRPCForbidden()
@@ -1,572 +1,570 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from rhodecode.api import (
21 from rhodecode.api import (
24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
22 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
25 from rhodecode.api.utils import (
23 from rhodecode.api.utils import (
26 Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update)
24 Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update)
27 from rhodecode.lib import audit_logger
25 from rhodecode.lib import audit_logger
28 from rhodecode.lib.auth import AuthUser, PasswordGenerator
26 from rhodecode.lib.auth import AuthUser, PasswordGenerator
29 from rhodecode.lib.exceptions import DefaultUserException
27 from rhodecode.lib.exceptions import DefaultUserException
30 from rhodecode.lib.utils2 import safe_int, str2bool
28 from rhodecode.lib.utils2 import safe_int, str2bool
31 from rhodecode.model.db import Session, User, Repository
29 from rhodecode.model.db import Session, User, Repository
32 from rhodecode.model.user import UserModel
30 from rhodecode.model.user import UserModel
33 from rhodecode.model import validation_schema
31 from rhodecode.model import validation_schema
34 from rhodecode.model.validation_schema.schemas import user_schema
32 from rhodecode.model.validation_schema.schemas import user_schema
35
33
36 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
37
35
38
36
39 @jsonrpc_method()
37 @jsonrpc_method()
40 def get_user(request, apiuser, userid=Optional(OAttr('apiuser'))):
38 def get_user(request, apiuser, userid=Optional(OAttr('apiuser'))):
41 """
39 """
42 Returns the information associated with a username or userid.
40 Returns the information associated with a username or userid.
43
41
44 * If the ``userid`` is not set, this command returns the information
42 * If the ``userid`` is not set, this command returns the information
45 for the ``userid`` calling the method.
43 for the ``userid`` calling the method.
46
44
47 .. note::
45 .. note::
48
46
49 Normal users may only run this command against their ``userid``. For
47 Normal users may only run this command against their ``userid``. For
50 full privileges you must run this command using an |authtoken| with
48 full privileges you must run this command using an |authtoken| with
51 admin rights.
49 admin rights.
52
50
53 :param apiuser: This is filled automatically from the |authtoken|.
51 :param apiuser: This is filled automatically from the |authtoken|.
54 :type apiuser: AuthUser
52 :type apiuser: AuthUser
55 :param userid: Sets the userid for which data will be returned.
53 :param userid: Sets the userid for which data will be returned.
56 :type userid: Optional(str or int)
54 :type userid: Optional(str or int)
57
55
58 Example output:
56 Example output:
59
57
60 .. code-block:: bash
58 .. code-block:: bash
61
59
62 {
60 {
63 "error": null,
61 "error": null,
64 "id": <id>,
62 "id": <id>,
65 "result": {
63 "result": {
66 "active": true,
64 "active": true,
67 "admin": false,
65 "admin": false,
68 "api_keys": [ list of keys ],
66 "api_keys": [ list of keys ],
69 "auth_tokens": [ list of tokens with details ],
67 "auth_tokens": [ list of tokens with details ],
70 "email": "user@example.com",
68 "email": "user@example.com",
71 "emails": [
69 "emails": [
72 "user@example.com"
70 "user@example.com"
73 ],
71 ],
74 "extern_name": "rhodecode",
72 "extern_name": "rhodecode",
75 "extern_type": "rhodecode",
73 "extern_type": "rhodecode",
76 "firstname": "username",
74 "firstname": "username",
77 "description": "user description",
75 "description": "user description",
78 "ip_addresses": [],
76 "ip_addresses": [],
79 "language": null,
77 "language": null,
80 "last_login": "Timestamp",
78 "last_login": "Timestamp",
81 "last_activity": "Timestamp",
79 "last_activity": "Timestamp",
82 "lastname": "surnae",
80 "lastname": "surnae",
83 "permissions": <deprecated>,
81 "permissions": <deprecated>,
84 "permissions_summary": {
82 "permissions_summary": {
85 "global": [
83 "global": [
86 "hg.inherit_default_perms.true",
84 "hg.inherit_default_perms.true",
87 "usergroup.read",
85 "usergroup.read",
88 "hg.repogroup.create.false",
86 "hg.repogroup.create.false",
89 "hg.create.none",
87 "hg.create.none",
90 "hg.password_reset.enabled",
88 "hg.password_reset.enabled",
91 "hg.extern_activate.manual",
89 "hg.extern_activate.manual",
92 "hg.create.write_on_repogroup.false",
90 "hg.create.write_on_repogroup.false",
93 "hg.usergroup.create.false",
91 "hg.usergroup.create.false",
94 "group.none",
92 "group.none",
95 "repository.none",
93 "repository.none",
96 "hg.register.none",
94 "hg.register.none",
97 "hg.fork.repository"
95 "hg.fork.repository"
98 ],
96 ],
99 "repositories": { "username/example": "repository.write"},
97 "repositories": { "username/example": "repository.write"},
100 "repositories_groups": { "user-group/repo": "group.none" },
98 "repositories_groups": { "user-group/repo": "group.none" },
101 "user_groups": { "user_group_name": "usergroup.read" }
99 "user_groups": { "user_group_name": "usergroup.read" }
102 }
100 }
103 "user_id": 32,
101 "user_id": 32,
104 "username": "username"
102 "username": "username"
105 }
103 }
106 }
104 }
107 """
105 """
108
106
109 if not has_superadmin_permission(apiuser):
107 if not has_superadmin_permission(apiuser):
110 # make sure normal user does not pass someone else userid,
108 # make sure normal user does not pass someone else userid,
111 # he is not allowed to do that
109 # he is not allowed to do that
112 if not isinstance(userid, Optional) and userid != apiuser.user_id:
110 if not isinstance(userid, Optional) and userid != apiuser.user_id:
113 raise JSONRPCError('userid is not the same as your user')
111 raise JSONRPCError('userid is not the same as your user')
114
112
115 userid = Optional.extract(userid, evaluate_locals=locals())
113 userid = Optional.extract(userid, evaluate_locals=locals())
116 userid = getattr(userid, 'user_id', userid)
114 userid = getattr(userid, 'user_id', userid)
117
115
118 user = get_user_or_error(userid)
116 user = get_user_or_error(userid)
119 data = user.get_api_data(include_secrets=True)
117 data = user.get_api_data(include_secrets=True)
120 permissions = AuthUser(user_id=user.user_id).permissions
118 permissions = AuthUser(user_id=user.user_id).permissions
121 data['permissions'] = permissions # TODO(marcink): should be deprecated
119 data['permissions'] = permissions # TODO(marcink): should be deprecated
122 data['permissions_summary'] = permissions
120 data['permissions_summary'] = permissions
123 return data
121 return data
124
122
125
123
126 @jsonrpc_method()
124 @jsonrpc_method()
127 def get_users(request, apiuser):
125 def get_users(request, apiuser):
128 """
126 """
129 Lists all users in the |RCE| user database.
127 Lists all users in the |RCE| user database.
130
128
131 This command can only be run using an |authtoken| with admin rights to
129 This command can only be run using an |authtoken| with admin rights to
132 the specified repository.
130 the specified repository.
133
131
134 This command takes the following options:
132 This command takes the following options:
135
133
136 :param apiuser: This is filled automatically from the |authtoken|.
134 :param apiuser: This is filled automatically from the |authtoken|.
137 :type apiuser: AuthUser
135 :type apiuser: AuthUser
138
136
139 Example output:
137 Example output:
140
138
141 .. code-block:: bash
139 .. code-block:: bash
142
140
143 id : <id_given_in_input>
141 id : <id_given_in_input>
144 result: [<user_object>, ...]
142 result: [<user_object>, ...]
145 error: null
143 error: null
146 """
144 """
147
145
148 if not has_superadmin_permission(apiuser):
146 if not has_superadmin_permission(apiuser):
149 raise JSONRPCForbidden()
147 raise JSONRPCForbidden()
150
148
151 result = []
149 result = []
152 users_list = User.query().order_by(User.username) \
150 users_list = User.query().order_by(User.username) \
153 .filter(User.username != User.DEFAULT_USER) \
151 .filter(User.username != User.DEFAULT_USER) \
154 .all()
152 .all()
155 for user in users_list:
153 for user in users_list:
156 result.append(user.get_api_data(include_secrets=True))
154 result.append(user.get_api_data(include_secrets=True))
157 return result
155 return result
158
156
159
157
160 @jsonrpc_method()
158 @jsonrpc_method()
161 def create_user(request, apiuser, username, email, password=Optional(''),
159 def create_user(request, apiuser, username, email, password=Optional(''),
162 firstname=Optional(''), lastname=Optional(''), description=Optional(''),
160 firstname=Optional(''), lastname=Optional(''), description=Optional(''),
163 active=Optional(True), admin=Optional(False),
161 active=Optional(True), admin=Optional(False),
164 extern_name=Optional('rhodecode'),
162 extern_name=Optional('rhodecode'),
165 extern_type=Optional('rhodecode'),
163 extern_type=Optional('rhodecode'),
166 force_password_change=Optional(False),
164 force_password_change=Optional(False),
167 create_personal_repo_group=Optional(None)):
165 create_personal_repo_group=Optional(None)):
168 """
166 """
169 Creates a new user and returns the new user object.
167 Creates a new user and returns the new user object.
170
168
171 This command can only be run using an |authtoken| with admin rights to
169 This command can only be run using an |authtoken| with admin rights to
172 the specified repository.
170 the specified repository.
173
171
174 This command takes the following options:
172 This command takes the following options:
175
173
176 :param apiuser: This is filled automatically from the |authtoken|.
174 :param apiuser: This is filled automatically from the |authtoken|.
177 :type apiuser: AuthUser
175 :type apiuser: AuthUser
178 :param username: Set the new username.
176 :param username: Set the new username.
179 :type username: str or int
177 :type username: str or int
180 :param email: Set the user email address.
178 :param email: Set the user email address.
181 :type email: str
179 :type email: str
182 :param password: Set the new user password.
180 :param password: Set the new user password.
183 :type password: Optional(str)
181 :type password: Optional(str)
184 :param firstname: Set the new user firstname.
182 :param firstname: Set the new user firstname.
185 :type firstname: Optional(str)
183 :type firstname: Optional(str)
186 :param lastname: Set the new user surname.
184 :param lastname: Set the new user surname.
187 :type lastname: Optional(str)
185 :type lastname: Optional(str)
188 :param description: Set user description, or short bio. Metatags are allowed.
186 :param description: Set user description, or short bio. Metatags are allowed.
189 :type description: Optional(str)
187 :type description: Optional(str)
190 :param active: Set the user as active.
188 :param active: Set the user as active.
191 :type active: Optional(``True`` | ``False``)
189 :type active: Optional(``True`` | ``False``)
192 :param admin: Give the new user admin rights.
190 :param admin: Give the new user admin rights.
193 :type admin: Optional(``True`` | ``False``)
191 :type admin: Optional(``True`` | ``False``)
194 :param extern_name: Set the authentication plugin name.
192 :param extern_name: Set the authentication plugin name.
195 Using LDAP this is filled with LDAP UID.
193 Using LDAP this is filled with LDAP UID.
196 :type extern_name: Optional(str)
194 :type extern_name: Optional(str)
197 :param extern_type: Set the new user authentication plugin.
195 :param extern_type: Set the new user authentication plugin.
198 :type extern_type: Optional(str)
196 :type extern_type: Optional(str)
199 :param force_password_change: Force the new user to change password
197 :param force_password_change: Force the new user to change password
200 on next login.
198 on next login.
201 :type force_password_change: Optional(``True`` | ``False``)
199 :type force_password_change: Optional(``True`` | ``False``)
202 :param create_personal_repo_group: Create personal repo group for this user
200 :param create_personal_repo_group: Create personal repo group for this user
203 :type create_personal_repo_group: Optional(``True`` | ``False``)
201 :type create_personal_repo_group: Optional(``True`` | ``False``)
204
202
205 Example output:
203 Example output:
206
204
207 .. code-block:: bash
205 .. code-block:: bash
208
206
209 id : <id_given_in_input>
207 id : <id_given_in_input>
210 result: {
208 result: {
211 "msg" : "created new user `<username>`",
209 "msg" : "created new user `<username>`",
212 "user": <user_obj>
210 "user": <user_obj>
213 }
211 }
214 error: null
212 error: null
215
213
216 Example error output:
214 Example error output:
217
215
218 .. code-block:: bash
216 .. code-block:: bash
219
217
220 id : <id_given_in_input>
218 id : <id_given_in_input>
221 result : null
219 result : null
222 error : {
220 error : {
223 "user `<username>` already exist"
221 "user `<username>` already exist"
224 or
222 or
225 "email `<email>` already exist"
223 "email `<email>` already exist"
226 or
224 or
227 "failed to create user `<username>`"
225 "failed to create user `<username>`"
228 }
226 }
229
227
230 """
228 """
231 if not has_superadmin_permission(apiuser):
229 if not has_superadmin_permission(apiuser):
232 raise JSONRPCForbidden()
230 raise JSONRPCForbidden()
233
231
234 if UserModel().get_by_username(username):
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 if UserModel().get_by_email(email, case_insensitive=True):
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 # generate random password if we actually given the
238 # generate random password if we actually given the
241 # extern_name and it's not rhodecode
239 # extern_name and it's not rhodecode
242 if (not isinstance(extern_name, Optional) and
240 if (not isinstance(extern_name, Optional) and
243 Optional.extract(extern_name) != 'rhodecode'):
241 Optional.extract(extern_name) != 'rhodecode'):
244 # generate temporary password if user is external
242 # generate temporary password if user is external
245 password = PasswordGenerator().gen_password(length=16)
243 password = PasswordGenerator().gen_password(length=16)
246 create_repo_group = Optional.extract(create_personal_repo_group)
244 create_repo_group = Optional.extract(create_personal_repo_group)
247 if isinstance(create_repo_group, str):
245 if isinstance(create_repo_group, str):
248 create_repo_group = str2bool(create_repo_group)
246 create_repo_group = str2bool(create_repo_group)
249
247
250 username = Optional.extract(username)
248 username = Optional.extract(username)
251 password = Optional.extract(password)
249 password = Optional.extract(password)
252 email = Optional.extract(email)
250 email = Optional.extract(email)
253 first_name = Optional.extract(firstname)
251 first_name = Optional.extract(firstname)
254 last_name = Optional.extract(lastname)
252 last_name = Optional.extract(lastname)
255 description = Optional.extract(description)
253 description = Optional.extract(description)
256 active = Optional.extract(active)
254 active = Optional.extract(active)
257 admin = Optional.extract(admin)
255 admin = Optional.extract(admin)
258 extern_type = Optional.extract(extern_type)
256 extern_type = Optional.extract(extern_type)
259 extern_name = Optional.extract(extern_name)
257 extern_name = Optional.extract(extern_name)
260
258
261 schema = user_schema.UserSchema().bind(
259 schema = user_schema.UserSchema().bind(
262 # user caller
260 # user caller
263 user=apiuser)
261 user=apiuser)
264 try:
262 try:
265 schema_data = schema.deserialize(dict(
263 schema_data = schema.deserialize(dict(
266 username=username,
264 username=username,
267 email=email,
265 email=email,
268 password=password,
266 password=password,
269 first_name=first_name,
267 first_name=first_name,
270 last_name=last_name,
268 last_name=last_name,
271 active=active,
269 active=active,
272 admin=admin,
270 admin=admin,
273 description=description,
271 description=description,
274 extern_type=extern_type,
272 extern_type=extern_type,
275 extern_name=extern_name,
273 extern_name=extern_name,
276 ))
274 ))
277 except validation_schema.Invalid as err:
275 except validation_schema.Invalid as err:
278 raise JSONRPCValidationError(colander_exc=err)
276 raise JSONRPCValidationError(colander_exc=err)
279
277
280 try:
278 try:
281 user = UserModel().create_or_update(
279 user = UserModel().create_or_update(
282 username=schema_data['username'],
280 username=schema_data['username'],
283 password=schema_data['password'],
281 password=schema_data['password'],
284 email=schema_data['email'],
282 email=schema_data['email'],
285 firstname=schema_data['first_name'],
283 firstname=schema_data['first_name'],
286 lastname=schema_data['last_name'],
284 lastname=schema_data['last_name'],
287 description=schema_data['description'],
285 description=schema_data['description'],
288 active=schema_data['active'],
286 active=schema_data['active'],
289 admin=schema_data['admin'],
287 admin=schema_data['admin'],
290 extern_type=schema_data['extern_type'],
288 extern_type=schema_data['extern_type'],
291 extern_name=schema_data['extern_name'],
289 extern_name=schema_data['extern_name'],
292 force_password_change=Optional.extract(force_password_change),
290 force_password_change=Optional.extract(force_password_change),
293 create_repo_group=create_repo_group
291 create_repo_group=create_repo_group
294 )
292 )
295 Session().flush()
293 Session().flush()
296 creation_data = user.get_api_data()
294 creation_data = user.get_api_data()
297 audit_logger.store_api(
295 audit_logger.store_api(
298 'user.create', action_data={'data': creation_data},
296 'user.create', action_data={'data': creation_data},
299 user=apiuser)
297 user=apiuser)
300
298
301 Session().commit()
299 Session().commit()
302 return {
300 return {
303 'msg': 'created new user `%s`' % username,
301 'msg': 'created new user `%s`' % username,
304 'user': user.get_api_data(include_secrets=True)
302 'user': user.get_api_data(include_secrets=True)
305 }
303 }
306 except Exception:
304 except Exception:
307 log.exception('Error occurred during creation of user')
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 @jsonrpc_method()
309 @jsonrpc_method()
312 def update_user(request, apiuser, userid, username=Optional(None),
310 def update_user(request, apiuser, userid, username=Optional(None),
313 email=Optional(None), password=Optional(None),
311 email=Optional(None), password=Optional(None),
314 firstname=Optional(None), lastname=Optional(None),
312 firstname=Optional(None), lastname=Optional(None),
315 description=Optional(None), active=Optional(None), admin=Optional(None),
313 description=Optional(None), active=Optional(None), admin=Optional(None),
316 extern_type=Optional(None), extern_name=Optional(None), ):
314 extern_type=Optional(None), extern_name=Optional(None), ):
317 """
315 """
318 Updates the details for the specified user, if that user exists.
316 Updates the details for the specified user, if that user exists.
319
317
320 This command can only be run using an |authtoken| with admin rights to
318 This command can only be run using an |authtoken| with admin rights to
321 the specified repository.
319 the specified repository.
322
320
323 This command takes the following options:
321 This command takes the following options:
324
322
325 :param apiuser: This is filled automatically from |authtoken|.
323 :param apiuser: This is filled automatically from |authtoken|.
326 :type apiuser: AuthUser
324 :type apiuser: AuthUser
327 :param userid: Set the ``userid`` to update.
325 :param userid: Set the ``userid`` to update.
328 :type userid: str or int
326 :type userid: str or int
329 :param username: Set the new username.
327 :param username: Set the new username.
330 :type username: str or int
328 :type username: str or int
331 :param email: Set the new email.
329 :param email: Set the new email.
332 :type email: str
330 :type email: str
333 :param password: Set the new password.
331 :param password: Set the new password.
334 :type password: Optional(str)
332 :type password: Optional(str)
335 :param firstname: Set the new first name.
333 :param firstname: Set the new first name.
336 :type firstname: Optional(str)
334 :type firstname: Optional(str)
337 :param lastname: Set the new surname.
335 :param lastname: Set the new surname.
338 :type lastname: Optional(str)
336 :type lastname: Optional(str)
339 :param description: Set user description, or short bio. Metatags are allowed.
337 :param description: Set user description, or short bio. Metatags are allowed.
340 :type description: Optional(str)
338 :type description: Optional(str)
341 :param active: Set the new user as active.
339 :param active: Set the new user as active.
342 :type active: Optional(``True`` | ``False``)
340 :type active: Optional(``True`` | ``False``)
343 :param admin: Give the user admin rights.
341 :param admin: Give the user admin rights.
344 :type admin: Optional(``True`` | ``False``)
342 :type admin: Optional(``True`` | ``False``)
345 :param extern_name: Set the authentication plugin user name.
343 :param extern_name: Set the authentication plugin user name.
346 Using LDAP this is filled with LDAP UID.
344 Using LDAP this is filled with LDAP UID.
347 :type extern_name: Optional(str)
345 :type extern_name: Optional(str)
348 :param extern_type: Set the authentication plugin type.
346 :param extern_type: Set the authentication plugin type.
349 :type extern_type: Optional(str)
347 :type extern_type: Optional(str)
350
348
351
349
352 Example output:
350 Example output:
353
351
354 .. code-block:: bash
352 .. code-block:: bash
355
353
356 id : <id_given_in_input>
354 id : <id_given_in_input>
357 result: {
355 result: {
358 "msg" : "updated user ID:<userid> <username>",
356 "msg" : "updated user ID:<userid> <username>",
359 "user": <user_object>,
357 "user": <user_object>,
360 }
358 }
361 error: null
359 error: null
362
360
363 Example error output:
361 Example error output:
364
362
365 .. code-block:: bash
363 .. code-block:: bash
366
364
367 id : <id_given_in_input>
365 id : <id_given_in_input>
368 result : null
366 result : null
369 error : {
367 error : {
370 "failed to update user `<username>`"
368 "failed to update user `<username>`"
371 }
369 }
372
370
373 """
371 """
374 if not has_superadmin_permission(apiuser):
372 if not has_superadmin_permission(apiuser):
375 raise JSONRPCForbidden()
373 raise JSONRPCForbidden()
376
374
377 user = get_user_or_error(userid)
375 user = get_user_or_error(userid)
378 old_data = user.get_api_data()
376 old_data = user.get_api_data()
379 # only non optional arguments will be stored in updates
377 # only non optional arguments will be stored in updates
380 updates = {}
378 updates = {}
381
379
382 try:
380 try:
383
381
384 store_update(updates, username, 'username')
382 store_update(updates, username, 'username')
385 store_update(updates, password, 'password')
383 store_update(updates, password, 'password')
386 store_update(updates, email, 'email')
384 store_update(updates, email, 'email')
387 store_update(updates, firstname, 'name')
385 store_update(updates, firstname, 'name')
388 store_update(updates, lastname, 'lastname')
386 store_update(updates, lastname, 'lastname')
389 store_update(updates, description, 'description')
387 store_update(updates, description, 'description')
390 store_update(updates, active, 'active')
388 store_update(updates, active, 'active')
391 store_update(updates, admin, 'admin')
389 store_update(updates, admin, 'admin')
392 store_update(updates, extern_name, 'extern_name')
390 store_update(updates, extern_name, 'extern_name')
393 store_update(updates, extern_type, 'extern_type')
391 store_update(updates, extern_type, 'extern_type')
394
392
395 user = UserModel().update_user(user, **updates)
393 user = UserModel().update_user(user, **updates)
396 audit_logger.store_api(
394 audit_logger.store_api(
397 'user.edit', action_data={'old_data': old_data},
395 'user.edit', action_data={'old_data': old_data},
398 user=apiuser)
396 user=apiuser)
399 Session().commit()
397 Session().commit()
400 return {
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 'user': user.get_api_data(include_secrets=True)
400 'user': user.get_api_data(include_secrets=True)
403 }
401 }
404 except DefaultUserException:
402 except DefaultUserException:
405 log.exception("Default user edit exception")
403 log.exception("Default user edit exception")
406 raise JSONRPCError('editing default user is forbidden')
404 raise JSONRPCError('editing default user is forbidden')
407 except Exception:
405 except Exception:
408 log.exception("Error occurred during update of user")
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 @jsonrpc_method()
410 @jsonrpc_method()
413 def delete_user(request, apiuser, userid):
411 def delete_user(request, apiuser, userid):
414 """
412 """
415 Deletes the specified user from the |RCE| user database.
413 Deletes the specified user from the |RCE| user database.
416
414
417 This command can only be run using an |authtoken| with admin rights to
415 This command can only be run using an |authtoken| with admin rights to
418 the specified repository.
416 the specified repository.
419
417
420 .. important::
418 .. important::
421
419
422 Ensure all open pull requests and open code review
420 Ensure all open pull requests and open code review
423 requests to this user are close.
421 requests to this user are close.
424
422
425 Also ensure all repositories, or repository groups owned by this
423 Also ensure all repositories, or repository groups owned by this
426 user are reassigned before deletion.
424 user are reassigned before deletion.
427
425
428 This command takes the following options:
426 This command takes the following options:
429
427
430 :param apiuser: This is filled automatically from the |authtoken|.
428 :param apiuser: This is filled automatically from the |authtoken|.
431 :type apiuser: AuthUser
429 :type apiuser: AuthUser
432 :param userid: Set the user to delete.
430 :param userid: Set the user to delete.
433 :type userid: str or int
431 :type userid: str or int
434
432
435 Example output:
433 Example output:
436
434
437 .. code-block:: bash
435 .. code-block:: bash
438
436
439 id : <id_given_in_input>
437 id : <id_given_in_input>
440 result: {
438 result: {
441 "msg" : "deleted user ID:<userid> <username>",
439 "msg" : "deleted user ID:<userid> <username>",
442 "user": null
440 "user": null
443 }
441 }
444 error: null
442 error: null
445
443
446 Example error output:
444 Example error output:
447
445
448 .. code-block:: bash
446 .. code-block:: bash
449
447
450 id : <id_given_in_input>
448 id : <id_given_in_input>
451 result : null
449 result : null
452 error : {
450 error : {
453 "failed to delete user ID:<userid> <username>"
451 "failed to delete user ID:<userid> <username>"
454 }
452 }
455
453
456 """
454 """
457 if not has_superadmin_permission(apiuser):
455 if not has_superadmin_permission(apiuser):
458 raise JSONRPCForbidden()
456 raise JSONRPCForbidden()
459
457
460 user = get_user_or_error(userid)
458 user = get_user_or_error(userid)
461 old_data = user.get_api_data()
459 old_data = user.get_api_data()
462 try:
460 try:
463 UserModel().delete(userid)
461 UserModel().delete(userid)
464 audit_logger.store_api(
462 audit_logger.store_api(
465 'user.delete', action_data={'old_data': old_data},
463 'user.delete', action_data={'old_data': old_data},
466 user=apiuser)
464 user=apiuser)
467
465
468 Session().commit()
466 Session().commit()
469 return {
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 'user': None
469 'user': None
472 }
470 }
473 except Exception:
471 except Exception:
474 log.exception("Error occurred during deleting of user")
472 log.exception("Error occurred during deleting of user")
475 raise JSONRPCError(
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 @jsonrpc_method()
477 @jsonrpc_method()
480 def get_user_locks(request, apiuser, userid=Optional(OAttr('apiuser'))):
478 def get_user_locks(request, apiuser, userid=Optional(OAttr('apiuser'))):
481 """
479 """
482 Displays all repositories locked by the specified user.
480 Displays all repositories locked by the specified user.
483
481
484 * If this command is run by a non-admin user, it returns
482 * If this command is run by a non-admin user, it returns
485 a list of |repos| locked by that user.
483 a list of |repos| locked by that user.
486
484
487 This command takes the following options:
485 This command takes the following options:
488
486
489 :param apiuser: This is filled automatically from the |authtoken|.
487 :param apiuser: This is filled automatically from the |authtoken|.
490 :type apiuser: AuthUser
488 :type apiuser: AuthUser
491 :param userid: Sets the userid whose list of locked |repos| will be
489 :param userid: Sets the userid whose list of locked |repos| will be
492 displayed.
490 displayed.
493 :type userid: Optional(str or int)
491 :type userid: Optional(str or int)
494
492
495 Example output:
493 Example output:
496
494
497 .. code-block:: bash
495 .. code-block:: bash
498
496
499 id : <id_given_in_input>
497 id : <id_given_in_input>
500 result : {
498 result : {
501 [repo_object, repo_object,...]
499 [repo_object, repo_object,...]
502 }
500 }
503 error : null
501 error : null
504 """
502 """
505
503
506 include_secrets = False
504 include_secrets = False
507 if not has_superadmin_permission(apiuser):
505 if not has_superadmin_permission(apiuser):
508 # make sure normal user does not pass someone else userid,
506 # make sure normal user does not pass someone else userid,
509 # he is not allowed to do that
507 # he is not allowed to do that
510 if not isinstance(userid, Optional) and userid != apiuser.user_id:
508 if not isinstance(userid, Optional) and userid != apiuser.user_id:
511 raise JSONRPCError('userid is not the same as your user')
509 raise JSONRPCError('userid is not the same as your user')
512 else:
510 else:
513 include_secrets = True
511 include_secrets = True
514
512
515 userid = Optional.extract(userid, evaluate_locals=locals())
513 userid = Optional.extract(userid, evaluate_locals=locals())
516 userid = getattr(userid, 'user_id', userid)
514 userid = getattr(userid, 'user_id', userid)
517 user = get_user_or_error(userid)
515 user = get_user_or_error(userid)
518
516
519 ret = []
517 ret = []
520
518
521 # show all locks
519 # show all locks
522 for r in Repository.getAll():
520 for r in Repository.getAll():
523 _user_id, _time, _reason = r.locked
521 _user_id, _time, _reason = r.locked
524 if _user_id and _time:
522 if _user_id and _time:
525 _api_data = r.get_api_data(include_secrets=include_secrets)
523 _api_data = r.get_api_data(include_secrets=include_secrets)
526 # if we use user filter just show the locks for this user
524 # if we use user filter just show the locks for this user
527 if safe_int(_user_id) == user.user_id:
525 if safe_int(_user_id) == user.user_id:
528 ret.append(_api_data)
526 ret.append(_api_data)
529
527
530 return ret
528 return ret
531
529
532
530
533 @jsonrpc_method()
531 @jsonrpc_method()
534 def get_user_audit_logs(request, apiuser, userid=Optional(OAttr('apiuser'))):
532 def get_user_audit_logs(request, apiuser, userid=Optional(OAttr('apiuser'))):
535 """
533 """
536 Fetches all action logs made by the specified user.
534 Fetches all action logs made by the specified user.
537
535
538 This command takes the following options:
536 This command takes the following options:
539
537
540 :param apiuser: This is filled automatically from the |authtoken|.
538 :param apiuser: This is filled automatically from the |authtoken|.
541 :type apiuser: AuthUser
539 :type apiuser: AuthUser
542 :param userid: Sets the userid whose list of locked |repos| will be
540 :param userid: Sets the userid whose list of locked |repos| will be
543 displayed.
541 displayed.
544 :type userid: Optional(str or int)
542 :type userid: Optional(str or int)
545
543
546 Example output:
544 Example output:
547
545
548 .. code-block:: bash
546 .. code-block:: bash
549
547
550 id : <id_given_in_input>
548 id : <id_given_in_input>
551 result : {
549 result : {
552 [action, action,...]
550 [action, action,...]
553 }
551 }
554 error : null
552 error : null
555 """
553 """
556
554
557 if not has_superadmin_permission(apiuser):
555 if not has_superadmin_permission(apiuser):
558 # make sure normal user does not pass someone else userid,
556 # make sure normal user does not pass someone else userid,
559 # he is not allowed to do that
557 # he is not allowed to do that
560 if not isinstance(userid, Optional) and userid != apiuser.user_id:
558 if not isinstance(userid, Optional) and userid != apiuser.user_id:
561 raise JSONRPCError('userid is not the same as your user')
559 raise JSONRPCError('userid is not the same as your user')
562
560
563 userid = Optional.extract(userid, evaluate_locals=locals())
561 userid = Optional.extract(userid, evaluate_locals=locals())
564 userid = getattr(userid, 'user_id', userid)
562 userid = getattr(userid, 'user_id', userid)
565 user = get_user_or_error(userid)
563 user = get_user_or_error(userid)
566
564
567 ret = []
565 ret = []
568
566
569 # show all user actions
567 # show all user actions
570 for entry in UserModel().get_user_log(user, filter_term=None):
568 for entry in UserModel().get_user_log(user, filter_term=None):
571 ret.append(entry)
569 ret.append(entry)
572 return ret
570 return ret
@@ -1,907 +1,905 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from rhodecode.api import (
21 from rhodecode.api import (
24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
22 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
25 from rhodecode.api.utils import (
23 from rhodecode.api.utils import (
26 Optional, OAttr, store_update, has_superadmin_permission, get_origin,
24 Optional, OAttr, store_update, has_superadmin_permission, get_origin,
27 get_user_or_error, get_user_group_or_error, get_perm_or_error)
25 get_user_or_error, get_user_group_or_error, get_perm_or_error)
28 from rhodecode.lib import audit_logger
26 from rhodecode.lib import audit_logger
29 from rhodecode.lib.auth import HasUserGroupPermissionAnyApi, HasPermissionAnyApi
27 from rhodecode.lib.auth import HasUserGroupPermissionAnyApi, HasPermissionAnyApi
30 from rhodecode.lib.exceptions import UserGroupAssignedException
28 from rhodecode.lib.exceptions import UserGroupAssignedException
31 from rhodecode.model.db import Session
29 from rhodecode.model.db import Session
32 from rhodecode.model.permission import PermissionModel
30 from rhodecode.model.permission import PermissionModel
33 from rhodecode.model.scm import UserGroupList
31 from rhodecode.model.scm import UserGroupList
34 from rhodecode.model.user_group import UserGroupModel
32 from rhodecode.model.user_group import UserGroupModel
35 from rhodecode.model import validation_schema
33 from rhodecode.model import validation_schema
36 from rhodecode.model.validation_schema.schemas import user_group_schema
34 from rhodecode.model.validation_schema.schemas import user_group_schema
37
35
38 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
39
37
40
38
41 @jsonrpc_method()
39 @jsonrpc_method()
42 def get_user_group(request, apiuser, usergroupid):
40 def get_user_group(request, apiuser, usergroupid):
43 """
41 """
44 Returns the data of an existing user group.
42 Returns the data of an existing user group.
45
43
46 This command can only be run using an |authtoken| with admin rights to
44 This command can only be run using an |authtoken| with admin rights to
47 the specified repository.
45 the specified repository.
48
46
49 :param apiuser: This is filled automatically from the |authtoken|.
47 :param apiuser: This is filled automatically from the |authtoken|.
50 :type apiuser: AuthUser
48 :type apiuser: AuthUser
51 :param usergroupid: Set the user group from which to return data.
49 :param usergroupid: Set the user group from which to return data.
52 :type usergroupid: str or int
50 :type usergroupid: str or int
53
51
54 Example error output:
52 Example error output:
55
53
56 .. code-block:: bash
54 .. code-block:: bash
57
55
58 {
56 {
59 "error": null,
57 "error": null,
60 "id": <id>,
58 "id": <id>,
61 "result": {
59 "result": {
62 "active": true,
60 "active": true,
63 "group_description": "group description",
61 "group_description": "group description",
64 "group_name": "group name",
62 "group_name": "group name",
65 "permissions": [
63 "permissions": [
66 {
64 {
67 "name": "owner-name",
65 "name": "owner-name",
68 "origin": "owner",
66 "origin": "owner",
69 "permission": "usergroup.admin",
67 "permission": "usergroup.admin",
70 "type": "user"
68 "type": "user"
71 },
69 },
72 {
70 {
73 {
71 {
74 "name": "user name",
72 "name": "user name",
75 "origin": "permission",
73 "origin": "permission",
76 "permission": "usergroup.admin",
74 "permission": "usergroup.admin",
77 "type": "user"
75 "type": "user"
78 },
76 },
79 {
77 {
80 "name": "user group name",
78 "name": "user group name",
81 "origin": "permission",
79 "origin": "permission",
82 "permission": "usergroup.write",
80 "permission": "usergroup.write",
83 "type": "user_group"
81 "type": "user_group"
84 }
82 }
85 ],
83 ],
86 "permissions_summary": {
84 "permissions_summary": {
87 "repositories": {
85 "repositories": {
88 "aa-root-level-repo-1": "repository.admin"
86 "aa-root-level-repo-1": "repository.admin"
89 },
87 },
90 "repositories_groups": {}
88 "repositories_groups": {}
91 },
89 },
92 "owner": "owner name",
90 "owner": "owner name",
93 "users": [],
91 "users": [],
94 "users_group_id": 2
92 "users_group_id": 2
95 }
93 }
96 }
94 }
97
95
98 """
96 """
99
97
100 user_group = get_user_group_or_error(usergroupid)
98 user_group = get_user_group_or_error(usergroupid)
101 if not has_superadmin_permission(apiuser):
99 if not has_superadmin_permission(apiuser):
102 # check if we have at least read permission for this user group !
100 # check if we have at least read permission for this user group !
103 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
101 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
104 if not HasUserGroupPermissionAnyApi(*_perms)(
102 if not HasUserGroupPermissionAnyApi(*_perms)(
105 user=apiuser, user_group_name=user_group.users_group_name):
103 user=apiuser, user_group_name=user_group.users_group_name):
106 raise JSONRPCError('user group `%s` does not exist' % (
104 raise JSONRPCError('user group `{}` does not exist'.format(
107 usergroupid,))
105 usergroupid))
108
106
109 permissions = []
107 permissions = []
110 for _user in user_group.permissions():
108 for _user in user_group.permissions():
111 user_data = {
109 user_data = {
112 'name': _user.username,
110 'name': _user.username,
113 'permission': _user.permission,
111 'permission': _user.permission,
114 'origin': get_origin(_user),
112 'origin': get_origin(_user),
115 'type': "user",
113 'type': "user",
116 }
114 }
117 permissions.append(user_data)
115 permissions.append(user_data)
118
116
119 for _user_group in user_group.permission_user_groups():
117 for _user_group in user_group.permission_user_groups():
120 user_group_data = {
118 user_group_data = {
121 'name': _user_group.users_group_name,
119 'name': _user_group.users_group_name,
122 'permission': _user_group.permission,
120 'permission': _user_group.permission,
123 'origin': get_origin(_user_group),
121 'origin': get_origin(_user_group),
124 'type': "user_group",
122 'type': "user_group",
125 }
123 }
126 permissions.append(user_group_data)
124 permissions.append(user_group_data)
127
125
128 data = user_group.get_api_data()
126 data = user_group.get_api_data()
129 data["permissions"] = permissions
127 data["permissions"] = permissions
130 data["permissions_summary"] = UserGroupModel().get_perms_summary(
128 data["permissions_summary"] = UserGroupModel().get_perms_summary(
131 user_group.users_group_id)
129 user_group.users_group_id)
132 return data
130 return data
133
131
134
132
135 @jsonrpc_method()
133 @jsonrpc_method()
136 def get_user_groups(request, apiuser):
134 def get_user_groups(request, apiuser):
137 """
135 """
138 Lists all the existing user groups within RhodeCode.
136 Lists all the existing user groups within RhodeCode.
139
137
140 This command can only be run using an |authtoken| with admin rights to
138 This command can only be run using an |authtoken| with admin rights to
141 the specified repository.
139 the specified repository.
142
140
143 This command takes the following options:
141 This command takes the following options:
144
142
145 :param apiuser: This is filled automatically from the |authtoken|.
143 :param apiuser: This is filled automatically from the |authtoken|.
146 :type apiuser: AuthUser
144 :type apiuser: AuthUser
147
145
148 Example error output:
146 Example error output:
149
147
150 .. code-block:: bash
148 .. code-block:: bash
151
149
152 id : <id_given_in_input>
150 id : <id_given_in_input>
153 result : [<user_group_obj>,...]
151 result : [<user_group_obj>,...]
154 error : null
152 error : null
155 """
153 """
156
154
157 include_secrets = has_superadmin_permission(apiuser)
155 include_secrets = has_superadmin_permission(apiuser)
158
156
159 result = []
157 result = []
160 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
158 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
161 extras = {'user': apiuser}
159 extras = {'user': apiuser}
162 for user_group in UserGroupList(UserGroupModel().get_all(),
160 for user_group in UserGroupList(UserGroupModel().get_all(),
163 perm_set=_perms, extra_kwargs=extras):
161 perm_set=_perms, extra_kwargs=extras):
164 result.append(
162 result.append(
165 user_group.get_api_data(include_secrets=include_secrets))
163 user_group.get_api_data(include_secrets=include_secrets))
166 return result
164 return result
167
165
168
166
169 @jsonrpc_method()
167 @jsonrpc_method()
170 def create_user_group(
168 def create_user_group(
171 request, apiuser, group_name, description=Optional(''),
169 request, apiuser, group_name, description=Optional(''),
172 owner=Optional(OAttr('apiuser')), active=Optional(True),
170 owner=Optional(OAttr('apiuser')), active=Optional(True),
173 sync=Optional(None)):
171 sync=Optional(None)):
174 """
172 """
175 Creates a new user group.
173 Creates a new user group.
176
174
177 This command can only be run using an |authtoken| with admin rights to
175 This command can only be run using an |authtoken| with admin rights to
178 the specified repository.
176 the specified repository.
179
177
180 This command takes the following options:
178 This command takes the following options:
181
179
182 :param apiuser: This is filled automatically from the |authtoken|.
180 :param apiuser: This is filled automatically from the |authtoken|.
183 :type apiuser: AuthUser
181 :type apiuser: AuthUser
184 :param group_name: Set the name of the new user group.
182 :param group_name: Set the name of the new user group.
185 :type group_name: str
183 :type group_name: str
186 :param description: Give a description of the new user group.
184 :param description: Give a description of the new user group.
187 :type description: str
185 :type description: str
188 :param owner: Set the owner of the new user group.
186 :param owner: Set the owner of the new user group.
189 If not set, the owner is the |authtoken| user.
187 If not set, the owner is the |authtoken| user.
190 :type owner: Optional(str or int)
188 :type owner: Optional(str or int)
191 :param active: Set this group as active.
189 :param active: Set this group as active.
192 :type active: Optional(``True`` | ``False``)
190 :type active: Optional(``True`` | ``False``)
193 :param sync: Set enabled or disabled the automatically sync from
191 :param sync: Set enabled or disabled the automatically sync from
194 external authentication types like ldap. If User Group will be named like
192 external authentication types like ldap. If User Group will be named like
195 one from e.g ldap and sync flag is enabled members will be synced automatically.
193 one from e.g ldap and sync flag is enabled members will be synced automatically.
196 Sync type when enabled via API is set to `manual_api`
194 Sync type when enabled via API is set to `manual_api`
197 :type sync: Optional(``True`` | ``False``)
195 :type sync: Optional(``True`` | ``False``)
198
196
199 Example output:
197 Example output:
200
198
201 .. code-block:: bash
199 .. code-block:: bash
202
200
203 id : <id_given_in_input>
201 id : <id_given_in_input>
204 result: {
202 result: {
205 "msg": "created new user group `<groupname>`",
203 "msg": "created new user group `<groupname>`",
206 "user_group": <user_group_object>
204 "user_group": <user_group_object>
207 }
205 }
208 error: null
206 error: null
209
207
210 Example error output:
208 Example error output:
211
209
212 .. code-block:: bash
210 .. code-block:: bash
213
211
214 id : <id_given_in_input>
212 id : <id_given_in_input>
215 result : null
213 result : null
216 error : {
214 error : {
217 "user group `<group name>` already exist"
215 "user group `<group name>` already exist"
218 or
216 or
219 "failed to create group `<group name>`"
217 "failed to create group `<group name>`"
220 }
218 }
221
219
222 """
220 """
223
221
224 if not has_superadmin_permission(apiuser):
222 if not has_superadmin_permission(apiuser):
225 if not HasPermissionAnyApi('hg.usergroup.create.true')(user=apiuser):
223 if not HasPermissionAnyApi('hg.usergroup.create.true')(user=apiuser):
226 raise JSONRPCForbidden()
224 raise JSONRPCForbidden()
227
225
228 if UserGroupModel().get_by_name(group_name):
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 if isinstance(owner, Optional):
229 if isinstance(owner, Optional):
232 owner = apiuser.user_id
230 owner = apiuser.user_id
233
231
234 owner = get_user_or_error(owner)
232 owner = get_user_or_error(owner)
235 active = Optional.extract(active)
233 active = Optional.extract(active)
236 description = Optional.extract(description)
234 description = Optional.extract(description)
237 sync = Optional.extract(sync)
235 sync = Optional.extract(sync)
238
236
239 # set the sync option based on group_data
237 # set the sync option based on group_data
240 group_data = None
238 group_data = None
241 if sync:
239 if sync:
242 group_data = {
240 group_data = {
243 'extern_type': 'manual_api',
241 'extern_type': 'manual_api',
244 'extern_type_set_by': apiuser.username
242 'extern_type_set_by': apiuser.username
245 }
243 }
246
244
247 schema = user_group_schema.UserGroupSchema().bind(
245 schema = user_group_schema.UserGroupSchema().bind(
248 # user caller
246 # user caller
249 user=apiuser)
247 user=apiuser)
250 try:
248 try:
251 schema_data = schema.deserialize(dict(
249 schema_data = schema.deserialize(dict(
252 user_group_name=group_name,
250 user_group_name=group_name,
253 user_group_description=description,
251 user_group_description=description,
254 user_group_owner=owner.username,
252 user_group_owner=owner.username,
255 user_group_active=active,
253 user_group_active=active,
256 ))
254 ))
257 except validation_schema.Invalid as err:
255 except validation_schema.Invalid as err:
258 raise JSONRPCValidationError(colander_exc=err)
256 raise JSONRPCValidationError(colander_exc=err)
259
257
260 try:
258 try:
261 user_group = UserGroupModel().create(
259 user_group = UserGroupModel().create(
262 name=schema_data['user_group_name'],
260 name=schema_data['user_group_name'],
263 description=schema_data['user_group_description'],
261 description=schema_data['user_group_description'],
264 owner=owner,
262 owner=owner,
265 active=schema_data['user_group_active'], group_data=group_data)
263 active=schema_data['user_group_active'], group_data=group_data)
266 Session().flush()
264 Session().flush()
267 creation_data = user_group.get_api_data()
265 creation_data = user_group.get_api_data()
268 audit_logger.store_api(
266 audit_logger.store_api(
269 'user_group.create', action_data={'data': creation_data},
267 'user_group.create', action_data={'data': creation_data},
270 user=apiuser)
268 user=apiuser)
271 Session().commit()
269 Session().commit()
272
270
273 affected_user_ids = [apiuser.user_id, owner.user_id]
271 affected_user_ids = [apiuser.user_id, owner.user_id]
274 PermissionModel().trigger_permission_flush(affected_user_ids)
272 PermissionModel().trigger_permission_flush(affected_user_ids)
275
273
276 return {
274 return {
277 'msg': 'created new user group `%s`' % group_name,
275 'msg': 'created new user group `%s`' % group_name,
278 'user_group': creation_data
276 'user_group': creation_data
279 }
277 }
280 except Exception:
278 except Exception:
281 log.exception("Error occurred during creation of user group")
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 @jsonrpc_method()
283 @jsonrpc_method()
286 def update_user_group(request, apiuser, usergroupid, group_name=Optional(''),
284 def update_user_group(request, apiuser, usergroupid, group_name=Optional(''),
287 description=Optional(''), owner=Optional(None),
285 description=Optional(''), owner=Optional(None),
288 active=Optional(True), sync=Optional(None)):
286 active=Optional(True), sync=Optional(None)):
289 """
287 """
290 Updates the specified `user group` with the details provided.
288 Updates the specified `user group` with the details provided.
291
289
292 This command can only be run using an |authtoken| with admin rights to
290 This command can only be run using an |authtoken| with admin rights to
293 the specified repository.
291 the specified repository.
294
292
295 :param apiuser: This is filled automatically from the |authtoken|.
293 :param apiuser: This is filled automatically from the |authtoken|.
296 :type apiuser: AuthUser
294 :type apiuser: AuthUser
297 :param usergroupid: Set the id of the `user group` to update.
295 :param usergroupid: Set the id of the `user group` to update.
298 :type usergroupid: str or int
296 :type usergroupid: str or int
299 :param group_name: Set the new name the `user group`
297 :param group_name: Set the new name the `user group`
300 :type group_name: str
298 :type group_name: str
301 :param description: Give a description for the `user group`
299 :param description: Give a description for the `user group`
302 :type description: str
300 :type description: str
303 :param owner: Set the owner of the `user group`.
301 :param owner: Set the owner of the `user group`.
304 :type owner: Optional(str or int)
302 :type owner: Optional(str or int)
305 :param active: Set the group as active.
303 :param active: Set the group as active.
306 :type active: Optional(``True`` | ``False``)
304 :type active: Optional(``True`` | ``False``)
307 :param sync: Set enabled or disabled the automatically sync from
305 :param sync: Set enabled or disabled the automatically sync from
308 external authentication types like ldap. If User Group will be named like
306 external authentication types like ldap. If User Group will be named like
309 one from e.g ldap and sync flag is enabled members will be synced automatically.
307 one from e.g ldap and sync flag is enabled members will be synced automatically.
310 Sync type when enabled via API is set to `manual_api`
308 Sync type when enabled via API is set to `manual_api`
311 :type sync: Optional(``True`` | ``False``)
309 :type sync: Optional(``True`` | ``False``)
312
310
313 Example output:
311 Example output:
314
312
315 .. code-block:: bash
313 .. code-block:: bash
316
314
317 id : <id_given_in_input>
315 id : <id_given_in_input>
318 result : {
316 result : {
319 "msg": 'updated user group ID:<user group id> <user group name>',
317 "msg": 'updated user group ID:<user group id> <user group name>',
320 "user_group": <user_group_object>
318 "user_group": <user_group_object>
321 }
319 }
322 error : null
320 error : null
323
321
324 Example error output:
322 Example error output:
325
323
326 .. code-block:: bash
324 .. code-block:: bash
327
325
328 id : <id_given_in_input>
326 id : <id_given_in_input>
329 result : null
327 result : null
330 error : {
328 error : {
331 "failed to update user group `<user group name>`"
329 "failed to update user group `<user group name>`"
332 }
330 }
333
331
334 """
332 """
335
333
336 user_group = get_user_group_or_error(usergroupid)
334 user_group = get_user_group_or_error(usergroupid)
337 include_secrets = False
335 include_secrets = False
338 if not has_superadmin_permission(apiuser):
336 if not has_superadmin_permission(apiuser):
339 # check if we have admin permission for this user group !
337 # check if we have admin permission for this user group !
340 _perms = ('usergroup.admin',)
338 _perms = ('usergroup.admin',)
341 if not HasUserGroupPermissionAnyApi(*_perms)(
339 if not HasUserGroupPermissionAnyApi(*_perms)(
342 user=apiuser, user_group_name=user_group.users_group_name):
340 user=apiuser, user_group_name=user_group.users_group_name):
343 raise JSONRPCError(
341 raise JSONRPCError(
344 'user group `%s` does not exist' % (usergroupid,))
342 'user group `{}` does not exist'.format(usergroupid))
345 else:
343 else:
346 include_secrets = True
344 include_secrets = True
347
345
348 if not isinstance(owner, Optional):
346 if not isinstance(owner, Optional):
349 owner = get_user_or_error(owner)
347 owner = get_user_or_error(owner)
350
348
351 old_data = user_group.get_api_data()
349 old_data = user_group.get_api_data()
352 updates = {}
350 updates = {}
353 store_update(updates, group_name, 'users_group_name')
351 store_update(updates, group_name, 'users_group_name')
354 store_update(updates, description, 'user_group_description')
352 store_update(updates, description, 'user_group_description')
355 store_update(updates, owner, 'user')
353 store_update(updates, owner, 'user')
356 store_update(updates, active, 'users_group_active')
354 store_update(updates, active, 'users_group_active')
357
355
358 sync = Optional.extract(sync)
356 sync = Optional.extract(sync)
359 group_data = None
357 group_data = None
360 if sync is True:
358 if sync is True:
361 group_data = {
359 group_data = {
362 'extern_type': 'manual_api',
360 'extern_type': 'manual_api',
363 'extern_type_set_by': apiuser.username
361 'extern_type_set_by': apiuser.username
364 }
362 }
365 if sync is False:
363 if sync is False:
366 group_data = user_group.group_data
364 group_data = user_group.group_data
367 if group_data and "extern_type" in group_data:
365 if group_data and "extern_type" in group_data:
368 del group_data["extern_type"]
366 del group_data["extern_type"]
369
367
370 try:
368 try:
371 UserGroupModel().update(user_group, updates, group_data=group_data)
369 UserGroupModel().update(user_group, updates, group_data=group_data)
372 audit_logger.store_api(
370 audit_logger.store_api(
373 'user_group.edit', action_data={'old_data': old_data},
371 'user_group.edit', action_data={'old_data': old_data},
374 user=apiuser)
372 user=apiuser)
375 Session().commit()
373 Session().commit()
376 return {
374 return {
377 'msg': 'updated user group ID:%s %s' % (
375 'msg': 'updated user group ID:{} {}'.format(
378 user_group.users_group_id, user_group.users_group_name),
376 user_group.users_group_id, user_group.users_group_name),
379 'user_group': user_group.get_api_data(
377 'user_group': user_group.get_api_data(
380 include_secrets=include_secrets)
378 include_secrets=include_secrets)
381 }
379 }
382 except Exception:
380 except Exception:
383 log.exception("Error occurred during update of user group")
381 log.exception("Error occurred during update of user group")
384 raise JSONRPCError(
382 raise JSONRPCError(
385 'failed to update user group `%s`' % (usergroupid,))
383 'failed to update user group `{}`'.format(usergroupid))
386
384
387
385
388 @jsonrpc_method()
386 @jsonrpc_method()
389 def delete_user_group(request, apiuser, usergroupid):
387 def delete_user_group(request, apiuser, usergroupid):
390 """
388 """
391 Deletes the specified `user group`.
389 Deletes the specified `user group`.
392
390
393 This command can only be run using an |authtoken| with admin rights to
391 This command can only be run using an |authtoken| with admin rights to
394 the specified repository.
392 the specified repository.
395
393
396 This command takes the following options:
394 This command takes the following options:
397
395
398 :param apiuser: filled automatically from apikey
396 :param apiuser: filled automatically from apikey
399 :type apiuser: AuthUser
397 :type apiuser: AuthUser
400 :param usergroupid:
398 :param usergroupid:
401 :type usergroupid: int
399 :type usergroupid: int
402
400
403 Example output:
401 Example output:
404
402
405 .. code-block:: bash
403 .. code-block:: bash
406
404
407 id : <id_given_in_input>
405 id : <id_given_in_input>
408 result : {
406 result : {
409 "msg": "deleted user group ID:<user_group_id> <user_group_name>"
407 "msg": "deleted user group ID:<user_group_id> <user_group_name>"
410 }
408 }
411 error : null
409 error : null
412
410
413 Example error output:
411 Example error output:
414
412
415 .. code-block:: bash
413 .. code-block:: bash
416
414
417 id : <id_given_in_input>
415 id : <id_given_in_input>
418 result : null
416 result : null
419 error : {
417 error : {
420 "failed to delete user group ID:<user_group_id> <user_group_name>"
418 "failed to delete user group ID:<user_group_id> <user_group_name>"
421 or
419 or
422 "RepoGroup assigned to <repo_groups_list>"
420 "RepoGroup assigned to <repo_groups_list>"
423 }
421 }
424
422
425 """
423 """
426
424
427 user_group = get_user_group_or_error(usergroupid)
425 user_group = get_user_group_or_error(usergroupid)
428 if not has_superadmin_permission(apiuser):
426 if not has_superadmin_permission(apiuser):
429 # check if we have admin permission for this user group !
427 # check if we have admin permission for this user group !
430 _perms = ('usergroup.admin',)
428 _perms = ('usergroup.admin',)
431 if not HasUserGroupPermissionAnyApi(*_perms)(
429 if not HasUserGroupPermissionAnyApi(*_perms)(
432 user=apiuser, user_group_name=user_group.users_group_name):
430 user=apiuser, user_group_name=user_group.users_group_name):
433 raise JSONRPCError(
431 raise JSONRPCError(
434 'user group `%s` does not exist' % (usergroupid,))
432 'user group `{}` does not exist'.format(usergroupid))
435
433
436 old_data = user_group.get_api_data()
434 old_data = user_group.get_api_data()
437 try:
435 try:
438 UserGroupModel().delete(user_group)
436 UserGroupModel().delete(user_group)
439 audit_logger.store_api(
437 audit_logger.store_api(
440 'user_group.delete', action_data={'old_data': old_data},
438 'user_group.delete', action_data={'old_data': old_data},
441 user=apiuser)
439 user=apiuser)
442 Session().commit()
440 Session().commit()
443 return {
441 return {
444 'msg': 'deleted user group ID:%s %s' % (
442 'msg': 'deleted user group ID:{} {}'.format(
445 user_group.users_group_id, user_group.users_group_name),
443 user_group.users_group_id, user_group.users_group_name),
446 'user_group': None
444 'user_group': None
447 }
445 }
448 except UserGroupAssignedException as e:
446 except UserGroupAssignedException as e:
449 log.exception("UserGroupAssigned error")
447 log.exception("UserGroupAssigned error")
450 raise JSONRPCError(str(e))
448 raise JSONRPCError(str(e))
451 except Exception:
449 except Exception:
452 log.exception("Error occurred during deletion of user group")
450 log.exception("Error occurred during deletion of user group")
453 raise JSONRPCError(
451 raise JSONRPCError(
454 'failed to delete user group ID:%s %s' %(
452 'failed to delete user group ID:%s %s' %(
455 user_group.users_group_id, user_group.users_group_name))
453 user_group.users_group_id, user_group.users_group_name))
456
454
457
455
458 @jsonrpc_method()
456 @jsonrpc_method()
459 def add_user_to_user_group(request, apiuser, usergroupid, userid):
457 def add_user_to_user_group(request, apiuser, usergroupid, userid):
460 """
458 """
461 Adds a user to a `user group`. If the user already exists in the group
459 Adds a user to a `user group`. If the user already exists in the group
462 this command will return false.
460 this command will return false.
463
461
464 This command can only be run using an |authtoken| with admin rights to
462 This command can only be run using an |authtoken| with admin rights to
465 the specified user group.
463 the specified user group.
466
464
467 This command takes the following options:
465 This command takes the following options:
468
466
469 :param apiuser: This is filled automatically from the |authtoken|.
467 :param apiuser: This is filled automatically from the |authtoken|.
470 :type apiuser: AuthUser
468 :type apiuser: AuthUser
471 :param usergroupid: Set the name of the `user group` to which a
469 :param usergroupid: Set the name of the `user group` to which a
472 user will be added.
470 user will be added.
473 :type usergroupid: int
471 :type usergroupid: int
474 :param userid: Set the `user_id` of the user to add to the group.
472 :param userid: Set the `user_id` of the user to add to the group.
475 :type userid: int
473 :type userid: int
476
474
477 Example output:
475 Example output:
478
476
479 .. code-block:: bash
477 .. code-block:: bash
480
478
481 id : <id_given_in_input>
479 id : <id_given_in_input>
482 result : {
480 result : {
483 "success": True|False # depends on if member is in group
481 "success": True|False # depends on if member is in group
484 "msg": "added member `<username>` to user group `<groupname>` |
482 "msg": "added member `<username>` to user group `<groupname>` |
485 User is already in that group"
483 User is already in that group"
486
484
487 }
485 }
488 error : null
486 error : null
489
487
490 Example error output:
488 Example error output:
491
489
492 .. code-block:: bash
490 .. code-block:: bash
493
491
494 id : <id_given_in_input>
492 id : <id_given_in_input>
495 result : null
493 result : null
496 error : {
494 error : {
497 "failed to add member to user group `<user_group_name>`"
495 "failed to add member to user group `<user_group_name>`"
498 }
496 }
499
497
500 """
498 """
501
499
502 user = get_user_or_error(userid)
500 user = get_user_or_error(userid)
503 user_group = get_user_group_or_error(usergroupid)
501 user_group = get_user_group_or_error(usergroupid)
504 if not has_superadmin_permission(apiuser):
502 if not has_superadmin_permission(apiuser):
505 # check if we have admin permission for this user group !
503 # check if we have admin permission for this user group !
506 _perms = ('usergroup.admin',)
504 _perms = ('usergroup.admin',)
507 if not HasUserGroupPermissionAnyApi(*_perms)(
505 if not HasUserGroupPermissionAnyApi(*_perms)(
508 user=apiuser, user_group_name=user_group.users_group_name):
506 user=apiuser, user_group_name=user_group.users_group_name):
509 raise JSONRPCError('user group `%s` does not exist' % (
507 raise JSONRPCError('user group `{}` does not exist'.format(
510 usergroupid,))
508 usergroupid))
511
509
512 old_values = user_group.get_api_data()
510 old_values = user_group.get_api_data()
513 try:
511 try:
514 ugm = UserGroupModel().add_user_to_group(user_group, user)
512 ugm = UserGroupModel().add_user_to_group(user_group, user)
515 success = True if ugm is not True else False
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 user.username, user_group.users_group_name
515 user.username, user_group.users_group_name
518 )
516 )
519 msg = msg if success else 'User is already in that group'
517 msg = msg if success else 'User is already in that group'
520 if success:
518 if success:
521 user_data = user.get_api_data()
519 user_data = user.get_api_data()
522 audit_logger.store_api(
520 audit_logger.store_api(
523 'user_group.edit.member.add',
521 'user_group.edit.member.add',
524 action_data={'user': user_data, 'old_data': old_values},
522 action_data={'user': user_data, 'old_data': old_values},
525 user=apiuser)
523 user=apiuser)
526
524
527 Session().commit()
525 Session().commit()
528
526
529 return {
527 return {
530 'success': success,
528 'success': success,
531 'msg': msg
529 'msg': msg
532 }
530 }
533 except Exception:
531 except Exception:
534 log.exception("Error occurred during adding a member to user group")
532 log.exception("Error occurred during adding a member to user group")
535 raise JSONRPCError(
533 raise JSONRPCError(
536 'failed to add member to user group `%s`' % (
534 'failed to add member to user group `{}`'.format(
537 user_group.users_group_name,
535 user_group.users_group_name,
538 )
536 )
539 )
537 )
540
538
541
539
542 @jsonrpc_method()
540 @jsonrpc_method()
543 def remove_user_from_user_group(request, apiuser, usergroupid, userid):
541 def remove_user_from_user_group(request, apiuser, usergroupid, userid):
544 """
542 """
545 Removes a user from a user group.
543 Removes a user from a user group.
546
544
547 * If the specified user is not in the group, this command will return
545 * If the specified user is not in the group, this command will return
548 `false`.
546 `false`.
549
547
550 This command can only be run using an |authtoken| with admin rights to
548 This command can only be run using an |authtoken| with admin rights to
551 the specified user group.
549 the specified user group.
552
550
553 :param apiuser: This is filled automatically from the |authtoken|.
551 :param apiuser: This is filled automatically from the |authtoken|.
554 :type apiuser: AuthUser
552 :type apiuser: AuthUser
555 :param usergroupid: Sets the user group name.
553 :param usergroupid: Sets the user group name.
556 :type usergroupid: str or int
554 :type usergroupid: str or int
557 :param userid: The user you wish to remove from |RCE|.
555 :param userid: The user you wish to remove from |RCE|.
558 :type userid: str or int
556 :type userid: str or int
559
557
560 Example output:
558 Example output:
561
559
562 .. code-block:: bash
560 .. code-block:: bash
563
561
564 id : <id_given_in_input>
562 id : <id_given_in_input>
565 result: {
563 result: {
566 "success": True|False, # depends on if member is in group
564 "success": True|False, # depends on if member is in group
567 "msg": "removed member <username> from user group <groupname> |
565 "msg": "removed member <username> from user group <groupname> |
568 User wasn't in group"
566 User wasn't in group"
569 }
567 }
570 error: null
568 error: null
571
569
572 """
570 """
573
571
574 user = get_user_or_error(userid)
572 user = get_user_or_error(userid)
575 user_group = get_user_group_or_error(usergroupid)
573 user_group = get_user_group_or_error(usergroupid)
576 if not has_superadmin_permission(apiuser):
574 if not has_superadmin_permission(apiuser):
577 # check if we have admin permission for this user group !
575 # check if we have admin permission for this user group !
578 _perms = ('usergroup.admin',)
576 _perms = ('usergroup.admin',)
579 if not HasUserGroupPermissionAnyApi(*_perms)(
577 if not HasUserGroupPermissionAnyApi(*_perms)(
580 user=apiuser, user_group_name=user_group.users_group_name):
578 user=apiuser, user_group_name=user_group.users_group_name):
581 raise JSONRPCError(
579 raise JSONRPCError(
582 'user group `%s` does not exist' % (usergroupid,))
580 'user group `{}` does not exist'.format(usergroupid))
583
581
584 old_values = user_group.get_api_data()
582 old_values = user_group.get_api_data()
585 try:
583 try:
586 success = UserGroupModel().remove_user_from_group(user_group, user)
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 user.username, user_group.users_group_name
586 user.username, user_group.users_group_name
589 )
587 )
590 msg = msg if success else "User wasn't in group"
588 msg = msg if success else "User wasn't in group"
591 if success:
589 if success:
592 user_data = user.get_api_data()
590 user_data = user.get_api_data()
593 audit_logger.store_api(
591 audit_logger.store_api(
594 'user_group.edit.member.delete',
592 'user_group.edit.member.delete',
595 action_data={'user': user_data, 'old_data': old_values},
593 action_data={'user': user_data, 'old_data': old_values},
596 user=apiuser)
594 user=apiuser)
597
595
598 Session().commit()
596 Session().commit()
599 return {'success': success, 'msg': msg}
597 return {'success': success, 'msg': msg}
600 except Exception:
598 except Exception:
601 log.exception("Error occurred during removing an member from user group")
599 log.exception("Error occurred during removing an member from user group")
602 raise JSONRPCError(
600 raise JSONRPCError(
603 'failed to remove member from user group `%s`' % (
601 'failed to remove member from user group `{}`'.format(
604 user_group.users_group_name,
602 user_group.users_group_name,
605 )
603 )
606 )
604 )
607
605
608
606
609 @jsonrpc_method()
607 @jsonrpc_method()
610 def grant_user_permission_to_user_group(
608 def grant_user_permission_to_user_group(
611 request, apiuser, usergroupid, userid, perm):
609 request, apiuser, usergroupid, userid, perm):
612 """
610 """
613 Set permissions for a user in a user group.
611 Set permissions for a user in a user group.
614
612
615 :param apiuser: This is filled automatically from the |authtoken|.
613 :param apiuser: This is filled automatically from the |authtoken|.
616 :type apiuser: AuthUser
614 :type apiuser: AuthUser
617 :param usergroupid: Set the user group to edit permissions on.
615 :param usergroupid: Set the user group to edit permissions on.
618 :type usergroupid: str or int
616 :type usergroupid: str or int
619 :param userid: Set the user from whom you wish to set permissions.
617 :param userid: Set the user from whom you wish to set permissions.
620 :type userid: str
618 :type userid: str
621 :param perm: (usergroup.(none|read|write|admin))
619 :param perm: (usergroup.(none|read|write|admin))
622 :type perm: str
620 :type perm: str
623
621
624 Example output:
622 Example output:
625
623
626 .. code-block:: bash
624 .. code-block:: bash
627
625
628 id : <id_given_in_input>
626 id : <id_given_in_input>
629 result : {
627 result : {
630 "msg": "Granted perm: `<perm_name>` for user: `<username>` in user group: `<user_group_name>`",
628 "msg": "Granted perm: `<perm_name>` for user: `<username>` in user group: `<user_group_name>`",
631 "success": true
629 "success": true
632 }
630 }
633 error : null
631 error : null
634 """
632 """
635
633
636 user_group = get_user_group_or_error(usergroupid)
634 user_group = get_user_group_or_error(usergroupid)
637
635
638 if not has_superadmin_permission(apiuser):
636 if not has_superadmin_permission(apiuser):
639 # check if we have admin permission for this user group !
637 # check if we have admin permission for this user group !
640 _perms = ('usergroup.admin',)
638 _perms = ('usergroup.admin',)
641 if not HasUserGroupPermissionAnyApi(*_perms)(
639 if not HasUserGroupPermissionAnyApi(*_perms)(
642 user=apiuser, user_group_name=user_group.users_group_name):
640 user=apiuser, user_group_name=user_group.users_group_name):
643 raise JSONRPCError(
641 raise JSONRPCError(
644 'user group `%s` does not exist' % (usergroupid,))
642 'user group `{}` does not exist'.format(usergroupid))
645
643
646 user = get_user_or_error(userid)
644 user = get_user_or_error(userid)
647 perm = get_perm_or_error(perm, prefix='usergroup.')
645 perm = get_perm_or_error(perm, prefix='usergroup.')
648
646
649 try:
647 try:
650 changes = UserGroupModel().grant_user_permission(
648 changes = UserGroupModel().grant_user_permission(
651 user_group=user_group, user=user, perm=perm)
649 user_group=user_group, user=user, perm=perm)
652
650
653 action_data = {
651 action_data = {
654 'added': changes['added'],
652 'added': changes['added'],
655 'updated': changes['updated'],
653 'updated': changes['updated'],
656 'deleted': changes['deleted'],
654 'deleted': changes['deleted'],
657 }
655 }
658 audit_logger.store_api(
656 audit_logger.store_api(
659 'user_group.edit.permissions', action_data=action_data,
657 'user_group.edit.permissions', action_data=action_data,
660 user=apiuser)
658 user=apiuser)
661 Session().commit()
659 Session().commit()
662 PermissionModel().flush_user_permission_caches(changes)
660 PermissionModel().flush_user_permission_caches(changes)
663
661
664 return {
662 return {
665 'msg':
663 'msg':
666 'Granted perm: `%s` for user: `%s` in user group: `%s`' % (
664 'Granted perm: `{}` for user: `{}` in user group: `{}`'.format(
667 perm.permission_name, user.username,
665 perm.permission_name, user.username,
668 user_group.users_group_name
666 user_group.users_group_name
669 ),
667 ),
670 'success': True
668 'success': True
671 }
669 }
672 except Exception:
670 except Exception:
673 log.exception("Error occurred during editing permissions "
671 log.exception("Error occurred during editing permissions "
674 "for user in user group")
672 "for user in user group")
675 raise JSONRPCError(
673 raise JSONRPCError(
676 'failed to edit permission for user: '
674 'failed to edit permission for user: '
677 '`%s` in user group: `%s`' % (
675 '`%s` in user group: `%s`' % (
678 userid, user_group.users_group_name))
676 userid, user_group.users_group_name))
679
677
680
678
681 @jsonrpc_method()
679 @jsonrpc_method()
682 def revoke_user_permission_from_user_group(
680 def revoke_user_permission_from_user_group(
683 request, apiuser, usergroupid, userid):
681 request, apiuser, usergroupid, userid):
684 """
682 """
685 Revoke a users permissions in a user group.
683 Revoke a users permissions in a user group.
686
684
687 :param apiuser: This is filled automatically from the |authtoken|.
685 :param apiuser: This is filled automatically from the |authtoken|.
688 :type apiuser: AuthUser
686 :type apiuser: AuthUser
689 :param usergroupid: Set the user group from which to revoke the user
687 :param usergroupid: Set the user group from which to revoke the user
690 permissions.
688 permissions.
691 :type: usergroupid: str or int
689 :type: usergroupid: str or int
692 :param userid: Set the userid of the user whose permissions will be
690 :param userid: Set the userid of the user whose permissions will be
693 revoked.
691 revoked.
694 :type userid: str
692 :type userid: str
695
693
696 Example output:
694 Example output:
697
695
698 .. code-block:: bash
696 .. code-block:: bash
699
697
700 id : <id_given_in_input>
698 id : <id_given_in_input>
701 result : {
699 result : {
702 "msg": "Revoked perm for user: `<username>` in user group: `<user_group_name>`",
700 "msg": "Revoked perm for user: `<username>` in user group: `<user_group_name>`",
703 "success": true
701 "success": true
704 }
702 }
705 error : null
703 error : null
706 """
704 """
707
705
708 user_group = get_user_group_or_error(usergroupid)
706 user_group = get_user_group_or_error(usergroupid)
709
707
710 if not has_superadmin_permission(apiuser):
708 if not has_superadmin_permission(apiuser):
711 # check if we have admin permission for this user group !
709 # check if we have admin permission for this user group !
712 _perms = ('usergroup.admin',)
710 _perms = ('usergroup.admin',)
713 if not HasUserGroupPermissionAnyApi(*_perms)(
711 if not HasUserGroupPermissionAnyApi(*_perms)(
714 user=apiuser, user_group_name=user_group.users_group_name):
712 user=apiuser, user_group_name=user_group.users_group_name):
715 raise JSONRPCError(
713 raise JSONRPCError(
716 'user group `%s` does not exist' % (usergroupid,))
714 'user group `{}` does not exist'.format(usergroupid))
717
715
718 user = get_user_or_error(userid)
716 user = get_user_or_error(userid)
719
717
720 try:
718 try:
721 changes = UserGroupModel().revoke_user_permission(
719 changes = UserGroupModel().revoke_user_permission(
722 user_group=user_group, user=user)
720 user_group=user_group, user=user)
723 action_data = {
721 action_data = {
724 'added': changes['added'],
722 'added': changes['added'],
725 'updated': changes['updated'],
723 'updated': changes['updated'],
726 'deleted': changes['deleted'],
724 'deleted': changes['deleted'],
727 }
725 }
728 audit_logger.store_api(
726 audit_logger.store_api(
729 'user_group.edit.permissions', action_data=action_data,
727 'user_group.edit.permissions', action_data=action_data,
730 user=apiuser)
728 user=apiuser)
731 Session().commit()
729 Session().commit()
732 PermissionModel().flush_user_permission_caches(changes)
730 PermissionModel().flush_user_permission_caches(changes)
733
731
734 return {
732 return {
735 'msg': 'Revoked perm for user: `%s` in user group: `%s`' % (
733 'msg': 'Revoked perm for user: `{}` in user group: `{}`'.format(
736 user.username, user_group.users_group_name
734 user.username, user_group.users_group_name
737 ),
735 ),
738 'success': True
736 'success': True
739 }
737 }
740 except Exception:
738 except Exception:
741 log.exception("Error occurred during editing permissions "
739 log.exception("Error occurred during editing permissions "
742 "for user in user group")
740 "for user in user group")
743 raise JSONRPCError(
741 raise JSONRPCError(
744 'failed to edit permission for user: `%s` in user group: `%s`'
742 'failed to edit permission for user: `%s` in user group: `%s`'
745 % (userid, user_group.users_group_name))
743 % (userid, user_group.users_group_name))
746
744
747
745
748 @jsonrpc_method()
746 @jsonrpc_method()
749 def grant_user_group_permission_to_user_group(
747 def grant_user_group_permission_to_user_group(
750 request, apiuser, usergroupid, sourceusergroupid, perm):
748 request, apiuser, usergroupid, sourceusergroupid, perm):
751 """
749 """
752 Give one user group permissions to another user group.
750 Give one user group permissions to another user group.
753
751
754 :param apiuser: This is filled automatically from the |authtoken|.
752 :param apiuser: This is filled automatically from the |authtoken|.
755 :type apiuser: AuthUser
753 :type apiuser: AuthUser
756 :param usergroupid: Set the user group on which to edit permissions.
754 :param usergroupid: Set the user group on which to edit permissions.
757 :type usergroupid: str or int
755 :type usergroupid: str or int
758 :param sourceusergroupid: Set the source user group to which
756 :param sourceusergroupid: Set the source user group to which
759 access/permissions will be granted.
757 access/permissions will be granted.
760 :type sourceusergroupid: str or int
758 :type sourceusergroupid: str or int
761 :param perm: (usergroup.(none|read|write|admin))
759 :param perm: (usergroup.(none|read|write|admin))
762 :type perm: str
760 :type perm: str
763
761
764 Example output:
762 Example output:
765
763
766 .. code-block:: bash
764 .. code-block:: bash
767
765
768 id : <id_given_in_input>
766 id : <id_given_in_input>
769 result : {
767 result : {
770 "msg": "Granted perm: `<perm_name>` for user group: `<source_user_group_name>` in user group: `<user_group_name>`",
768 "msg": "Granted perm: `<perm_name>` for user group: `<source_user_group_name>` in user group: `<user_group_name>`",
771 "success": true
769 "success": true
772 }
770 }
773 error : null
771 error : null
774 """
772 """
775
773
776 user_group = get_user_group_or_error(sourceusergroupid)
774 user_group = get_user_group_or_error(sourceusergroupid)
777 target_user_group = get_user_group_or_error(usergroupid)
775 target_user_group = get_user_group_or_error(usergroupid)
778 perm = get_perm_or_error(perm, prefix='usergroup.')
776 perm = get_perm_or_error(perm, prefix='usergroup.')
779
777
780 if not has_superadmin_permission(apiuser):
778 if not has_superadmin_permission(apiuser):
781 # check if we have admin permission for this user group !
779 # check if we have admin permission for this user group !
782 _perms = ('usergroup.admin',)
780 _perms = ('usergroup.admin',)
783 if not HasUserGroupPermissionAnyApi(*_perms)(
781 if not HasUserGroupPermissionAnyApi(*_perms)(
784 user=apiuser,
782 user=apiuser,
785 user_group_name=target_user_group.users_group_name):
783 user_group_name=target_user_group.users_group_name):
786 raise JSONRPCError(
784 raise JSONRPCError(
787 'to user group `%s` does not exist' % (usergroupid,))
785 'to user group `{}` does not exist'.format(usergroupid))
788
786
789 # check if we have at least read permission for source user group !
787 # check if we have at least read permission for source user group !
790 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
788 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
791 if not HasUserGroupPermissionAnyApi(*_perms)(
789 if not HasUserGroupPermissionAnyApi(*_perms)(
792 user=apiuser, user_group_name=user_group.users_group_name):
790 user=apiuser, user_group_name=user_group.users_group_name):
793 raise JSONRPCError(
791 raise JSONRPCError(
794 'user group `%s` does not exist' % (sourceusergroupid,))
792 'user group `{}` does not exist'.format(sourceusergroupid))
795
793
796 try:
794 try:
797 changes = UserGroupModel().grant_user_group_permission(
795 changes = UserGroupModel().grant_user_group_permission(
798 target_user_group=target_user_group,
796 target_user_group=target_user_group,
799 user_group=user_group, perm=perm)
797 user_group=user_group, perm=perm)
800
798
801 action_data = {
799 action_data = {
802 'added': changes['added'],
800 'added': changes['added'],
803 'updated': changes['updated'],
801 'updated': changes['updated'],
804 'deleted': changes['deleted'],
802 'deleted': changes['deleted'],
805 }
803 }
806 audit_logger.store_api(
804 audit_logger.store_api(
807 'user_group.edit.permissions', action_data=action_data,
805 'user_group.edit.permissions', action_data=action_data,
808 user=apiuser)
806 user=apiuser)
809 Session().commit()
807 Session().commit()
810 PermissionModel().flush_user_permission_caches(changes)
808 PermissionModel().flush_user_permission_caches(changes)
811
809
812 return {
810 return {
813 'msg': 'Granted perm: `%s` for user group: `%s` '
811 'msg': 'Granted perm: `%s` for user group: `%s` '
814 'in user group: `%s`' % (
812 'in user group: `%s`' % (
815 perm.permission_name, user_group.users_group_name,
813 perm.permission_name, user_group.users_group_name,
816 target_user_group.users_group_name
814 target_user_group.users_group_name
817 ),
815 ),
818 'success': True
816 'success': True
819 }
817 }
820 except Exception:
818 except Exception:
821 log.exception("Error occurred during editing permissions "
819 log.exception("Error occurred during editing permissions "
822 "for user group in user group")
820 "for user group in user group")
823 raise JSONRPCError(
821 raise JSONRPCError(
824 'failed to edit permission for user group: `%s` in '
822 'failed to edit permission for user group: `%s` in '
825 'user group: `%s`' % (
823 'user group: `%s`' % (
826 sourceusergroupid, target_user_group.users_group_name
824 sourceusergroupid, target_user_group.users_group_name
827 )
825 )
828 )
826 )
829
827
830
828
831 @jsonrpc_method()
829 @jsonrpc_method()
832 def revoke_user_group_permission_from_user_group(
830 def revoke_user_group_permission_from_user_group(
833 request, apiuser, usergroupid, sourceusergroupid):
831 request, apiuser, usergroupid, sourceusergroupid):
834 """
832 """
835 Revoke the permissions that one user group has to another.
833 Revoke the permissions that one user group has to another.
836
834
837 :param apiuser: This is filled automatically from the |authtoken|.
835 :param apiuser: This is filled automatically from the |authtoken|.
838 :type apiuser: AuthUser
836 :type apiuser: AuthUser
839 :param usergroupid: Set the user group on which to edit permissions.
837 :param usergroupid: Set the user group on which to edit permissions.
840 :type usergroupid: str or int
838 :type usergroupid: str or int
841 :param sourceusergroupid: Set the user group from which permissions
839 :param sourceusergroupid: Set the user group from which permissions
842 are revoked.
840 are revoked.
843 :type sourceusergroupid: str or int
841 :type sourceusergroupid: str or int
844
842
845 Example output:
843 Example output:
846
844
847 .. code-block:: bash
845 .. code-block:: bash
848
846
849 id : <id_given_in_input>
847 id : <id_given_in_input>
850 result : {
848 result : {
851 "msg": "Revoked perm for user group: `<user_group_name>` in user group: `<target_user_group_name>`",
849 "msg": "Revoked perm for user group: `<user_group_name>` in user group: `<target_user_group_name>`",
852 "success": true
850 "success": true
853 }
851 }
854 error : null
852 error : null
855 """
853 """
856
854
857 user_group = get_user_group_or_error(sourceusergroupid)
855 user_group = get_user_group_or_error(sourceusergroupid)
858 target_user_group = get_user_group_or_error(usergroupid)
856 target_user_group = get_user_group_or_error(usergroupid)
859
857
860 if not has_superadmin_permission(apiuser):
858 if not has_superadmin_permission(apiuser):
861 # check if we have admin permission for this user group !
859 # check if we have admin permission for this user group !
862 _perms = ('usergroup.admin',)
860 _perms = ('usergroup.admin',)
863 if not HasUserGroupPermissionAnyApi(*_perms)(
861 if not HasUserGroupPermissionAnyApi(*_perms)(
864 user=apiuser,
862 user=apiuser,
865 user_group_name=target_user_group.users_group_name):
863 user_group_name=target_user_group.users_group_name):
866 raise JSONRPCError(
864 raise JSONRPCError(
867 'to user group `%s` does not exist' % (usergroupid,))
865 'to user group `{}` does not exist'.format(usergroupid))
868
866
869 # check if we have at least read permission
867 # check if we have at least read permission
870 # for the source user group !
868 # for the source user group !
871 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
869 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
872 if not HasUserGroupPermissionAnyApi(*_perms)(
870 if not HasUserGroupPermissionAnyApi(*_perms)(
873 user=apiuser, user_group_name=user_group.users_group_name):
871 user=apiuser, user_group_name=user_group.users_group_name):
874 raise JSONRPCError(
872 raise JSONRPCError(
875 'user group `%s` does not exist' % (sourceusergroupid,))
873 'user group `{}` does not exist'.format(sourceusergroupid))
876
874
877 try:
875 try:
878 changes = UserGroupModel().revoke_user_group_permission(
876 changes = UserGroupModel().revoke_user_group_permission(
879 target_user_group=target_user_group, user_group=user_group)
877 target_user_group=target_user_group, user_group=user_group)
880 action_data = {
878 action_data = {
881 'added': changes['added'],
879 'added': changes['added'],
882 'updated': changes['updated'],
880 'updated': changes['updated'],
883 'deleted': changes['deleted'],
881 'deleted': changes['deleted'],
884 }
882 }
885 audit_logger.store_api(
883 audit_logger.store_api(
886 'user_group.edit.permissions', action_data=action_data,
884 'user_group.edit.permissions', action_data=action_data,
887 user=apiuser)
885 user=apiuser)
888 Session().commit()
886 Session().commit()
889 PermissionModel().flush_user_permission_caches(changes)
887 PermissionModel().flush_user_permission_caches(changes)
890
888
891 return {
889 return {
892 'msg': 'Revoked perm for user group: '
890 'msg': 'Revoked perm for user group: '
893 '`%s` in user group: `%s`' % (
891 '`%s` in user group: `%s`' % (
894 user_group.users_group_name,
892 user_group.users_group_name,
895 target_user_group.users_group_name
893 target_user_group.users_group_name
896 ),
894 ),
897 'success': True
895 'success': True
898 }
896 }
899 except Exception:
897 except Exception:
900 log.exception("Error occurred during editing permissions "
898 log.exception("Error occurred during editing permissions "
901 "for user group in user group")
899 "for user group in user group")
902 raise JSONRPCError(
900 raise JSONRPCError(
903 'failed to edit permission for user group: '
901 'failed to edit permission for user group: '
904 '`%s` in user group: `%s`' % (
902 '`%s` in user group: `%s`' % (
905 sourceusergroupid, target_user_group.users_group_name
903 sourceusergroupid, target_user_group.users_group_name
906 )
904 )
907 )
905 )
@@ -1,251 +1,251 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 This serves as a drop in replacement for pycurl. It implements the pycurl Curl
20 This serves as a drop in replacement for pycurl. It implements the pycurl Curl
21 class in a way that is compatible with gevent.
21 class in a way that is compatible with gevent.
22 """
22 """
23
23
24
24
25 import logging
25 import logging
26 import gevent
26 import gevent
27 import pycurl
27 import pycurl
28 import greenlet
28 import greenlet
29
29
30 # Import everything from pycurl.
30 # Import everything from pycurl.
31 # This allows us to use this module as a drop in replacement of pycurl.
31 # This allows us to use this module as a drop in replacement of pycurl.
32 from pycurl import * # pragma: no cover
32 from pycurl import * # pragma: no cover
33
33
34 from gevent import core
34 from gevent import core
35 from gevent.hub import Waiter
35 from gevent.hub import Waiter
36
36
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class GeventCurlMulti(object):
41 class GeventCurlMulti(object):
42 """
42 """
43 Wrapper around pycurl.CurlMulti that integrates it into gevent's event
43 Wrapper around pycurl.CurlMulti that integrates it into gevent's event
44 loop.
44 loop.
45
45
46 Parts of this class are a modified version of code copied from the Tornado
46 Parts of this class are a modified version of code copied from the Tornado
47 Web Server project which is licensed under the Apache License, Version 2.0
47 Web Server project which is licensed under the Apache License, Version 2.0
48 (the "License"). To be more specific the code originates from this file:
48 (the "License"). To be more specific the code originates from this file:
49 https://github.com/tornadoweb/tornado/blob/stable/tornado/curl_httpclient.py
49 https://github.com/tornadoweb/tornado/blob/stable/tornado/curl_httpclient.py
50
50
51 This is the original license header of the origin:
51 This is the original license header of the origin:
52
52
53 Copyright 2009 Facebook
53 Copyright 2009 Facebook
54
54
55 Licensed under the Apache License, Version 2.0 (the "License"); you may
55 Licensed under the Apache License, Version 2.0 (the "License"); you may
56 not use this file except in compliance with the License. You may obtain
56 not use this file except in compliance with the License. You may obtain
57 a copy of the License at
57 a copy of the License at
58
58
59 http://www.apache.org/licenses/LICENSE-2.0
59 http://www.apache.org/licenses/LICENSE-2.0
60
60
61 Unless required by applicable law or agreed to in writing, software
61 Unless required by applicable law or agreed to in writing, software
62 distributed under the License is distributed on an "AS IS" BASIS,
62 distributed under the License is distributed on an "AS IS" BASIS,
63 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
63 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
64 implied. See the License for the specific language governing
64 implied. See the License for the specific language governing
65 permissions and limitations under the License.
65 permissions and limitations under the License.
66 """
66 """
67
67
68 def __init__(self, loop=None):
68 def __init__(self, loop=None):
69 self._watchers = {}
69 self._watchers = {}
70 self._timeout = None
70 self._timeout = None
71 self.loop = loop or gevent.get_hub().loop
71 self.loop = loop or gevent.get_hub().loop
72
72
73 # Setup curl's multi instance.
73 # Setup curl's multi instance.
74 self._curl_multi = pycurl.CurlMulti()
74 self._curl_multi = pycurl.CurlMulti()
75 self.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout)
75 self.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout)
76 self.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket)
76 self.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket)
77
77
78 def __getattr__(self, item):
78 def __getattr__(self, item):
79 """
79 """
80 The pycurl.CurlMulti class is final and we cannot subclass it.
80 The pycurl.CurlMulti class is final and we cannot subclass it.
81 Therefore we are wrapping it and forward everything to it here.
81 Therefore we are wrapping it and forward everything to it here.
82 """
82 """
83 return getattr(self._curl_multi, item)
83 return getattr(self._curl_multi, item)
84
84
85 def add_handle(self, curl):
85 def add_handle(self, curl):
86 """
86 """
87 Add handle variant that also takes care about the initial invocation of
87 Add handle variant that also takes care about the initial invocation of
88 socket action method. This is done by setting an immediate timeout.
88 socket action method. This is done by setting an immediate timeout.
89 """
89 """
90 result = self._curl_multi.add_handle(curl)
90 result = self._curl_multi.add_handle(curl)
91 self._set_timeout(0)
91 self._set_timeout(0)
92 return result
92 return result
93
93
94 def _handle_socket(self, event, fd, multi, data):
94 def _handle_socket(self, event, fd, multi, data):
95 """
95 """
96 Called by libcurl when it wants to change the file descriptors it cares
96 Called by libcurl when it wants to change the file descriptors it cares
97 about.
97 about.
98 """
98 """
99 event_map = {
99 event_map = {
100 pycurl.POLL_NONE: core.NONE,
100 pycurl.POLL_NONE: core.NONE,
101 pycurl.POLL_IN: core.READ,
101 pycurl.POLL_IN: core.READ,
102 pycurl.POLL_OUT: core.WRITE,
102 pycurl.POLL_OUT: core.WRITE,
103 pycurl.POLL_INOUT: core.READ | core.WRITE
103 pycurl.POLL_INOUT: core.READ | core.WRITE
104 }
104 }
105
105
106 if event == pycurl.POLL_REMOVE:
106 if event == pycurl.POLL_REMOVE:
107 watcher = self._watchers.pop(fd, None)
107 watcher = self._watchers.pop(fd, None)
108 if watcher is not None:
108 if watcher is not None:
109 watcher.stop()
109 watcher.stop()
110 else:
110 else:
111 gloop_event = event_map[event]
111 gloop_event = event_map[event]
112 watcher = self._watchers.get(fd)
112 watcher = self._watchers.get(fd)
113 if watcher is None:
113 if watcher is None:
114 watcher = self.loop.io(fd, gloop_event)
114 watcher = self.loop.io(fd, gloop_event)
115 watcher.start(self._handle_events, fd, pass_events=True)
115 watcher.start(self._handle_events, fd, pass_events=True)
116 self._watchers[fd] = watcher
116 self._watchers[fd] = watcher
117 else:
117 else:
118 if watcher.events != gloop_event:
118 if watcher.events != gloop_event:
119 watcher.stop()
119 watcher.stop()
120 watcher.events = gloop_event
120 watcher.events = gloop_event
121 watcher.start(self._handle_events, fd, pass_events=True)
121 watcher.start(self._handle_events, fd, pass_events=True)
122
122
123 def _set_timeout(self, msecs):
123 def _set_timeout(self, msecs):
124 """
124 """
125 Called by libcurl to schedule a timeout.
125 Called by libcurl to schedule a timeout.
126 """
126 """
127 if self._timeout is not None:
127 if self._timeout is not None:
128 self._timeout.stop()
128 self._timeout.stop()
129 self._timeout = self.loop.timer(msecs/1000.0)
129 self._timeout = self.loop.timer(msecs/1000.0)
130 self._timeout.start(self._handle_timeout)
130 self._timeout.start(self._handle_timeout)
131
131
132 def _handle_events(self, events, fd):
132 def _handle_events(self, events, fd):
133 action = 0
133 action = 0
134 if events & core.READ:
134 if events & core.READ:
135 action |= pycurl.CSELECT_IN
135 action |= pycurl.CSELECT_IN
136 if events & core.WRITE:
136 if events & core.WRITE:
137 action |= pycurl.CSELECT_OUT
137 action |= pycurl.CSELECT_OUT
138 while True:
138 while True:
139 try:
139 try:
140 ret, num_handles = self._curl_multi.socket_action(fd, action)
140 ret, num_handles = self._curl_multi.socket_action(fd, action)
141 except pycurl.error as e:
141 except pycurl.error as e:
142 ret = e.args[0]
142 ret = e.args[0]
143 if ret != pycurl.E_CALL_MULTI_PERFORM:
143 if ret != pycurl.E_CALL_MULTI_PERFORM:
144 break
144 break
145 self._finish_pending_requests()
145 self._finish_pending_requests()
146
146
147 def _handle_timeout(self):
147 def _handle_timeout(self):
148 """
148 """
149 Called by IOLoop when the requested timeout has passed.
149 Called by IOLoop when the requested timeout has passed.
150 """
150 """
151 if self._timeout is not None:
151 if self._timeout is not None:
152 self._timeout.stop()
152 self._timeout.stop()
153 self._timeout = None
153 self._timeout = None
154 while True:
154 while True:
155 try:
155 try:
156 ret, num_handles = self._curl_multi.socket_action(
156 ret, num_handles = self._curl_multi.socket_action(
157 pycurl.SOCKET_TIMEOUT, 0)
157 pycurl.SOCKET_TIMEOUT, 0)
158 except pycurl.error as e:
158 except pycurl.error as e:
159 ret = e.args[0]
159 ret = e.args[0]
160 if ret != pycurl.E_CALL_MULTI_PERFORM:
160 if ret != pycurl.E_CALL_MULTI_PERFORM:
161 break
161 break
162 self._finish_pending_requests()
162 self._finish_pending_requests()
163
163
164 # In theory, we shouldn't have to do this because curl will call
164 # In theory, we shouldn't have to do this because curl will call
165 # _set_timeout whenever the timeout changes. However, sometimes after
165 # _set_timeout whenever the timeout changes. However, sometimes after
166 # _handle_timeout we will need to reschedule immediately even though
166 # _handle_timeout we will need to reschedule immediately even though
167 # nothing has changed from curl's perspective. This is because when
167 # nothing has changed from curl's perspective. This is because when
168 # socket_action is called with SOCKET_TIMEOUT, libcurl decides
168 # socket_action is called with SOCKET_TIMEOUT, libcurl decides
169 # internally which timeouts need to be processed by using a monotonic
169 # internally which timeouts need to be processed by using a monotonic
170 # clock (where available) while tornado uses python's time.time() to
170 # clock (where available) while tornado uses python's time.time() to
171 # decide when timeouts have occurred. When those clocks disagree on
171 # decide when timeouts have occurred. When those clocks disagree on
172 # elapsed time (as they will whenever there is an NTP adjustment),
172 # elapsed time (as they will whenever there is an NTP adjustment),
173 # tornado might call _handle_timeout before libcurl is ready. After
173 # tornado might call _handle_timeout before libcurl is ready. After
174 # each timeout, resync the scheduled timeout with libcurl's current
174 # each timeout, resync the scheduled timeout with libcurl's current
175 # state.
175 # state.
176 new_timeout = self._curl_multi.timeout()
176 new_timeout = self._curl_multi.timeout()
177 if new_timeout >= 0:
177 if new_timeout >= 0:
178 self._set_timeout(new_timeout)
178 self._set_timeout(new_timeout)
179
179
180 def _finish_pending_requests(self):
180 def _finish_pending_requests(self):
181 """
181 """
182 Process any requests that were completed by the last call to
182 Process any requests that were completed by the last call to
183 multi.socket_action.
183 multi.socket_action.
184 """
184 """
185 while True:
185 while True:
186 num_q, ok_list, err_list = self._curl_multi.info_read()
186 num_q, ok_list, err_list = self._curl_multi.info_read()
187 for curl in ok_list:
187 for curl in ok_list:
188 curl.waiter.switch(None)
188 curl.waiter.switch(None)
189 for curl, errnum, errmsg in err_list:
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 if num_q == 0:
191 if num_q == 0:
192 break
192 break
193
193
194
194
195 class GeventCurl(object):
195 class GeventCurl(object):
196 """
196 """
197 Gevent compatible implementation of the pycurl.Curl class. Essentially a
197 Gevent compatible implementation of the pycurl.Curl class. Essentially a
198 wrapper around pycurl.Curl with a customized perform method. It uses the
198 wrapper around pycurl.Curl with a customized perform method. It uses the
199 GeventCurlMulti class to implement a blocking API to libcurl's "easy"
199 GeventCurlMulti class to implement a blocking API to libcurl's "easy"
200 interface.
200 interface.
201 """
201 """
202
202
203 # Reference to the GeventCurlMulti instance.
203 # Reference to the GeventCurlMulti instance.
204 _multi_instance = None
204 _multi_instance = None
205
205
206 def __init__(self):
206 def __init__(self):
207 self._curl = pycurl.Curl()
207 self._curl = pycurl.Curl()
208
208
209 def __getattr__(self, item):
209 def __getattr__(self, item):
210 """
210 """
211 The pycurl.Curl class is final and we cannot subclass it. Therefore we
211 The pycurl.Curl class is final and we cannot subclass it. Therefore we
212 are wrapping it and forward everything to it here.
212 are wrapping it and forward everything to it here.
213 """
213 """
214 return getattr(self._curl, item)
214 return getattr(self._curl, item)
215
215
216 @property
216 @property
217 def _multi(self):
217 def _multi(self):
218 """
218 """
219 Lazy property that returns the GeventCurlMulti instance. The value is
219 Lazy property that returns the GeventCurlMulti instance. The value is
220 cached as a class attribute. Therefore only one instance per process
220 cached as a class attribute. Therefore only one instance per process
221 exists.
221 exists.
222 """
222 """
223 if GeventCurl._multi_instance is None:
223 if GeventCurl._multi_instance is None:
224 GeventCurl._multi_instance = GeventCurlMulti()
224 GeventCurl._multi_instance = GeventCurlMulti()
225 return GeventCurl._multi_instance
225 return GeventCurl._multi_instance
226
226
227 def perform(self):
227 def perform(self):
228 """
228 """
229 This perform method is compatible with gevent because it uses gevent
229 This perform method is compatible with gevent because it uses gevent
230 synchronization mechanisms to wait for the request to finish.
230 synchronization mechanisms to wait for the request to finish.
231 """
231 """
232 if getattr(self._curl, 'waiter', None) is not None:
232 if getattr(self._curl, 'waiter', None) is not None:
233 current = greenlet.getcurrent()
233 current = greenlet.getcurrent()
234 msg = 'This curl object is already used by another greenlet, {}, \n' \
234 msg = 'This curl object is already used by another greenlet, {}, \n' \
235 'this is {}'.format(self._curl.waiter, current)
235 'this is {}'.format(self._curl.waiter, current)
236 raise Exception(msg)
236 raise Exception(msg)
237
237
238 waiter = self._curl.waiter = Waiter()
238 waiter = self._curl.waiter = Waiter()
239 try:
239 try:
240 self._multi.add_handle(self._curl)
240 self._multi.add_handle(self._curl)
241 try:
241 try:
242 return waiter.get()
242 return waiter.get()
243 finally:
243 finally:
244 self._multi.remove_handle(self._curl)
244 self._multi.remove_handle(self._curl)
245 finally:
245 finally:
246 del self._curl.waiter
246 del self._curl.waiter
247
247
248
248
249 # Curl is originally imported from pycurl. At this point we override it with
249 # Curl is originally imported from pycurl. At this point we override it with
250 # our custom implementation.
250 # our custom implementation.
251 Curl = GeventCurl
251 Curl = GeventCurl
@@ -1,962 +1,960 b''
1
2
3 # Copyright (C) 2014-2023 RhodeCode GmbH
1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 """
19 """
22 Module holding everything related to vcs nodes, with vcs2 architecture.
20 Module holding everything related to vcs nodes, with vcs2 architecture.
23 """
21 """
24 import functools
22 import functools
25 import os
23 import os
26 import stat
24 import stat
27
25
28 from zope.cachedescriptors.property import Lazy as LazyProperty
26 from zope.cachedescriptors.property import Lazy as LazyProperty
29
27
30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
28 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
31 from rhodecode.lib.str_utils import safe_str, safe_bytes
29 from rhodecode.lib.str_utils import safe_str, safe_bytes
32 from rhodecode.lib.hash_utils import md5
30 from rhodecode.lib.hash_utils import md5
33 from rhodecode.lib.vcs import path as vcspath
31 from rhodecode.lib.vcs import path as vcspath
34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
32 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
33 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
34 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
37
35
38 LARGEFILE_PREFIX = '.hglf'
36 LARGEFILE_PREFIX = '.hglf'
39
37
40
38
41 class NodeKind:
39 class NodeKind:
42 SUBMODULE = -1
40 SUBMODULE = -1
43 DIR = 1
41 DIR = 1
44 FILE = 2
42 FILE = 2
45 LARGEFILE = 3
43 LARGEFILE = 3
46
44
47
45
48 class NodeState:
46 class NodeState:
49 ADDED = 'added'
47 ADDED = 'added'
50 CHANGED = 'changed'
48 CHANGED = 'changed'
51 NOT_CHANGED = 'not changed'
49 NOT_CHANGED = 'not changed'
52 REMOVED = 'removed'
50 REMOVED = 'removed'
53
51
54 #TODO: not sure if that should be bytes or str ?
52 #TODO: not sure if that should be bytes or str ?
55 # most probably bytes because content should be bytes and we check it
53 # most probably bytes because content should be bytes and we check it
56 BIN_BYTE_MARKER = b'\0'
54 BIN_BYTE_MARKER = b'\0'
57
55
58
56
59 class NodeGeneratorBase(object):
57 class NodeGeneratorBase(object):
60 """
58 """
61 Base class for removed added and changed filenodes, it's a lazy generator
59 Base class for removed added and changed filenodes, it's a lazy generator
62 class that will create filenodes only on iteration or call
60 class that will create filenodes only on iteration or call
63
61
64 The len method doesn't need to create filenodes at all
62 The len method doesn't need to create filenodes at all
65 """
63 """
66
64
67 def __init__(self, current_paths, cs):
65 def __init__(self, current_paths, cs):
68 self.cs = cs
66 self.cs = cs
69 self.current_paths = current_paths
67 self.current_paths = current_paths
70
68
71 def __call__(self):
69 def __call__(self):
72 return [n for n in self]
70 return [n for n in self]
73
71
74 def __getitem__(self, key):
72 def __getitem__(self, key):
75 if isinstance(key, slice):
73 if isinstance(key, slice):
76 for p in self.current_paths[key.start:key.stop]:
74 for p in self.current_paths[key.start:key.stop]:
77 yield self.cs.get_node(p)
75 yield self.cs.get_node(p)
78
76
79 def __len__(self):
77 def __len__(self):
80 return len(self.current_paths)
78 return len(self.current_paths)
81
79
82 def __iter__(self):
80 def __iter__(self):
83 for p in self.current_paths:
81 for p in self.current_paths:
84 yield self.cs.get_node(p)
82 yield self.cs.get_node(p)
85
83
86
84
87 class AddedFileNodesGenerator(NodeGeneratorBase):
85 class AddedFileNodesGenerator(NodeGeneratorBase):
88 """
86 """
89 Class holding added files for current commit
87 Class holding added files for current commit
90 """
88 """
91
89
92
90
93 class ChangedFileNodesGenerator(NodeGeneratorBase):
91 class ChangedFileNodesGenerator(NodeGeneratorBase):
94 """
92 """
95 Class holding changed files for current commit
93 Class holding changed files for current commit
96 """
94 """
97
95
98
96
99 class RemovedFileNodesGenerator(NodeGeneratorBase):
97 class RemovedFileNodesGenerator(NodeGeneratorBase):
100 """
98 """
101 Class holding removed files for current commit
99 Class holding removed files for current commit
102 """
100 """
103 def __iter__(self):
101 def __iter__(self):
104 for p in self.current_paths:
102 for p in self.current_paths:
105 yield RemovedFileNode(path=safe_bytes(p))
103 yield RemovedFileNode(path=safe_bytes(p))
106
104
107 def __getitem__(self, key):
105 def __getitem__(self, key):
108 if isinstance(key, slice):
106 if isinstance(key, slice):
109 for p in self.current_paths[key.start:key.stop]:
107 for p in self.current_paths[key.start:key.stop]:
110 yield RemovedFileNode(path=safe_bytes(p))
108 yield RemovedFileNode(path=safe_bytes(p))
111
109
112
110
113 @functools.total_ordering
111 @functools.total_ordering
114 class Node(object):
112 class Node(object):
115 """
113 """
116 Simplest class representing file or directory on repository. SCM backends
114 Simplest class representing file or directory on repository. SCM backends
117 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
115 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
118 directly.
116 directly.
119
117
120 Node's ``path`` cannot start with slash as we operate on *relative* paths
118 Node's ``path`` cannot start with slash as we operate on *relative* paths
121 only. Moreover, every single node is identified by the ``path`` attribute,
119 only. Moreover, every single node is identified by the ``path`` attribute,
122 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
120 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
123 """
121 """
124 # RTLO marker allows swapping text, and certain
122 # RTLO marker allows swapping text, and certain
125 # security attacks could be used with this
123 # security attacks could be used with this
126 RTLO_MARKER = "\u202E"
124 RTLO_MARKER = "\u202E"
127
125
128 commit = None
126 commit = None
129
127
130 def __init__(self, path: bytes, kind):
128 def __init__(self, path: bytes, kind):
131 self._validate_path(path) # can throw exception if path is invalid
129 self._validate_path(path) # can throw exception if path is invalid
132
130
133 self.bytes_path = path.rstrip(b'/') # store for __repr__
131 self.bytes_path = path.rstrip(b'/') # store for __repr__
134 self.path = safe_str(self.bytes_path) # we store paths as str
132 self.path = safe_str(self.bytes_path) # we store paths as str
135
133
136 if self.bytes_path == b'' and kind != NodeKind.DIR:
134 if self.bytes_path == b'' and kind != NodeKind.DIR:
137 raise NodeError("Only DirNode and its subclasses may be "
135 raise NodeError("Only DirNode and its subclasses may be "
138 "initialized with empty path")
136 "initialized with empty path")
139 self.kind = kind
137 self.kind = kind
140
138
141 if self.is_root() and not self.is_dir():
139 if self.is_root() and not self.is_dir():
142 raise NodeError("Root node cannot be FILE kind")
140 raise NodeError("Root node cannot be FILE kind")
143
141
144 def __eq__(self, other):
142 def __eq__(self, other):
145 if type(self) is not type(other):
143 if type(self) is not type(other):
146 return False
144 return False
147 for attr in ['name', 'path', 'kind']:
145 for attr in ['name', 'path', 'kind']:
148 if getattr(self, attr) != getattr(other, attr):
146 if getattr(self, attr) != getattr(other, attr):
149 return False
147 return False
150 if self.is_file():
148 if self.is_file():
151 # FileNode compare, we need to fallback to content compare
149 # FileNode compare, we need to fallback to content compare
152 return None
150 return None
153 else:
151 else:
154 # For DirNode's check without entering each dir
152 # For DirNode's check without entering each dir
155 self_nodes_paths = list(sorted(n.path for n in self.nodes))
153 self_nodes_paths = list(sorted(n.path for n in self.nodes))
156 other_nodes_paths = list(sorted(n.path for n in self.nodes))
154 other_nodes_paths = list(sorted(n.path for n in self.nodes))
157 if self_nodes_paths != other_nodes_paths:
155 if self_nodes_paths != other_nodes_paths:
158 return False
156 return False
159 return True
157 return True
160
158
161 def __lt__(self, other):
159 def __lt__(self, other):
162 if self.kind < other.kind:
160 if self.kind < other.kind:
163 return True
161 return True
164 if self.kind > other.kind:
162 if self.kind > other.kind:
165 return False
163 return False
166 if self.path < other.path:
164 if self.path < other.path:
167 return True
165 return True
168 if self.path > other.path:
166 if self.path > other.path:
169 return False
167 return False
170
168
171 # def __cmp__(self, other):
169 # def __cmp__(self, other):
172 # """
170 # """
173 # Comparator using name of the node, needed for quick list sorting.
171 # Comparator using name of the node, needed for quick list sorting.
174 # """
172 # """
175 #
173 #
176 # kind_cmp = cmp(self.kind, other.kind)
174 # kind_cmp = cmp(self.kind, other.kind)
177 # if kind_cmp:
175 # if kind_cmp:
178 # if isinstance(self, SubModuleNode):
176 # if isinstance(self, SubModuleNode):
179 # # we make submodules equal to dirnode for "sorting" purposes
177 # # we make submodules equal to dirnode for "sorting" purposes
180 # return NodeKind.DIR
178 # return NodeKind.DIR
181 # return kind_cmp
179 # return kind_cmp
182 # return cmp(self.name, other.name)
180 # return cmp(self.name, other.name)
183
181
184 def __repr__(self):
182 def __repr__(self):
185 maybe_path = getattr(self, 'path', 'UNKNOWN_PATH')
183 maybe_path = getattr(self, 'path', 'UNKNOWN_PATH')
186 return f'<{self.__class__.__name__} {maybe_path!r}>'
184 return f'<{self.__class__.__name__} {maybe_path!r}>'
187
185
188 def __str__(self):
186 def __str__(self):
189 return self.name
187 return self.name
190
188
191 def _validate_path(self, path: bytes):
189 def _validate_path(self, path: bytes):
192 self._assert_bytes(path)
190 self._assert_bytes(path)
193
191
194 if path.startswith(b'/'):
192 if path.startswith(b'/'):
195 raise NodeError(
193 raise NodeError(
196 f"Cannot initialize Node objects with slash at "
194 f"Cannot initialize Node objects with slash at "
197 f"the beginning as only relative paths are supported. "
195 f"the beginning as only relative paths are supported. "
198 f"Got {path}")
196 f"Got {path}")
199
197
200 def _assert_bytes(self, value):
198 def _assert_bytes(self, value):
201 if not isinstance(value, bytes):
199 if not isinstance(value, bytes):
202 raise TypeError(f"Bytes required as input, got {type(value)} of {value}.")
200 raise TypeError(f"Bytes required as input, got {type(value)} of {value}.")
203
201
204 @LazyProperty
202 @LazyProperty
205 def parent(self):
203 def parent(self):
206 parent_path = self.get_parent_path()
204 parent_path = self.get_parent_path()
207 if parent_path:
205 if parent_path:
208 if self.commit:
206 if self.commit:
209 return self.commit.get_node(parent_path)
207 return self.commit.get_node(parent_path)
210 return DirNode(parent_path)
208 return DirNode(parent_path)
211 return None
209 return None
212
210
213 @LazyProperty
211 @LazyProperty
214 def str_path(self) -> str:
212 def str_path(self) -> str:
215 return safe_str(self.path)
213 return safe_str(self.path)
216
214
217 @LazyProperty
215 @LazyProperty
218 def has_rtlo(self):
216 def has_rtlo(self):
219 """Detects if a path has right-to-left-override marker"""
217 """Detects if a path has right-to-left-override marker"""
220 return self.RTLO_MARKER in self.str_path
218 return self.RTLO_MARKER in self.str_path
221
219
222 @LazyProperty
220 @LazyProperty
223 def dir_path(self):
221 def dir_path(self):
224 """
222 """
225 Returns name of the directory from full path of this vcs node. Empty
223 Returns name of the directory from full path of this vcs node. Empty
226 string is returned if there's no directory in the path
224 string is returned if there's no directory in the path
227 """
225 """
228 _parts = self.path.rstrip('/').rsplit('/', 1)
226 _parts = self.path.rstrip('/').rsplit('/', 1)
229 if len(_parts) == 2:
227 if len(_parts) == 2:
230 return _parts[0]
228 return _parts[0]
231 return ''
229 return ''
232
230
233 @LazyProperty
231 @LazyProperty
234 def name(self):
232 def name(self):
235 """
233 """
236 Returns name of the node so if its path
234 Returns name of the node so if its path
237 then only last part is returned.
235 then only last part is returned.
238 """
236 """
239 return self.path.rstrip('/').split('/')[-1]
237 return self.path.rstrip('/').split('/')[-1]
240
238
241 @property
239 @property
242 def kind(self):
240 def kind(self):
243 return self._kind
241 return self._kind
244
242
245 @kind.setter
243 @kind.setter
246 def kind(self, kind):
244 def kind(self, kind):
247 if hasattr(self, '_kind'):
245 if hasattr(self, '_kind'):
248 raise NodeError("Cannot change node's kind")
246 raise NodeError("Cannot change node's kind")
249 else:
247 else:
250 self._kind = kind
248 self._kind = kind
251 # Post setter check (path's trailing slash)
249 # Post setter check (path's trailing slash)
252 if self.path.endswith('/'):
250 if self.path.endswith('/'):
253 raise NodeError("Node's path cannot end with slash")
251 raise NodeError("Node's path cannot end with slash")
254
252
255 def get_parent_path(self) -> bytes:
253 def get_parent_path(self) -> bytes:
256 """
254 """
257 Returns node's parent path or empty string if node is root.
255 Returns node's parent path or empty string if node is root.
258 """
256 """
259 if self.is_root():
257 if self.is_root():
260 return b''
258 return b''
261 str_path = vcspath.dirname(self.path.rstrip('/')) + '/'
259 str_path = vcspath.dirname(self.path.rstrip('/')) + '/'
262
260
263 return safe_bytes(str_path)
261 return safe_bytes(str_path)
264
262
265 def is_file(self):
263 def is_file(self):
266 """
264 """
267 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
265 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
268 otherwise.
266 otherwise.
269 """
267 """
270 return self.kind == NodeKind.FILE
268 return self.kind == NodeKind.FILE
271
269
272 def is_dir(self):
270 def is_dir(self):
273 """
271 """
274 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
272 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
275 otherwise.
273 otherwise.
276 """
274 """
277 return self.kind == NodeKind.DIR
275 return self.kind == NodeKind.DIR
278
276
279 def is_root(self):
277 def is_root(self):
280 """
278 """
281 Returns ``True`` if node is a root node and ``False`` otherwise.
279 Returns ``True`` if node is a root node and ``False`` otherwise.
282 """
280 """
283 return self.kind == NodeKind.DIR and self.path == ''
281 return self.kind == NodeKind.DIR and self.path == ''
284
282
285 def is_submodule(self):
283 def is_submodule(self):
286 """
284 """
287 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
285 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
288 otherwise.
286 otherwise.
289 """
287 """
290 return self.kind == NodeKind.SUBMODULE
288 return self.kind == NodeKind.SUBMODULE
291
289
292 def is_largefile(self):
290 def is_largefile(self):
293 """
291 """
294 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
292 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
295 otherwise
293 otherwise
296 """
294 """
297 return self.kind == NodeKind.LARGEFILE
295 return self.kind == NodeKind.LARGEFILE
298
296
299 def is_link(self):
297 def is_link(self):
300 if self.commit:
298 if self.commit:
301 return self.commit.is_link(self.path)
299 return self.commit.is_link(self.path)
302 return False
300 return False
303
301
304 @LazyProperty
302 @LazyProperty
305 def added(self):
303 def added(self):
306 return self.state is NodeState.ADDED
304 return self.state is NodeState.ADDED
307
305
308 @LazyProperty
306 @LazyProperty
309 def changed(self):
307 def changed(self):
310 return self.state is NodeState.CHANGED
308 return self.state is NodeState.CHANGED
311
309
312 @LazyProperty
310 @LazyProperty
313 def not_changed(self):
311 def not_changed(self):
314 return self.state is NodeState.NOT_CHANGED
312 return self.state is NodeState.NOT_CHANGED
315
313
316 @LazyProperty
314 @LazyProperty
317 def removed(self):
315 def removed(self):
318 return self.state is NodeState.REMOVED
316 return self.state is NodeState.REMOVED
319
317
320
318
321 class FileNode(Node):
319 class FileNode(Node):
322 """
320 """
323 Class representing file nodes.
321 Class representing file nodes.
324
322
325 :attribute: path: path to the node, relative to repository's root
323 :attribute: path: path to the node, relative to repository's root
326 :attribute: content: if given arbitrary sets content of the file
324 :attribute: content: if given arbitrary sets content of the file
327 :attribute: commit: if given, first time content is accessed, callback
325 :attribute: commit: if given, first time content is accessed, callback
328 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
326 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
329 """
327 """
330 _filter_pre_load = []
328 _filter_pre_load = []
331
329
332 def __init__(self, path: bytes, content: bytes | None = None, commit=None, mode=None, pre_load=None):
330 def __init__(self, path: bytes, content: bytes | None = None, commit=None, mode=None, pre_load=None):
333 """
331 """
334 Only one of ``content`` and ``commit`` may be given. Passing both
332 Only one of ``content`` and ``commit`` may be given. Passing both
335 would raise ``NodeError`` exception.
333 would raise ``NodeError`` exception.
336
334
337 :param path: relative path to the node
335 :param path: relative path to the node
338 :param content: content may be passed to constructor
336 :param content: content may be passed to constructor
339 :param commit: if given, will use it to lazily fetch content
337 :param commit: if given, will use it to lazily fetch content
340 :param mode: ST_MODE (i.e. 0100644)
338 :param mode: ST_MODE (i.e. 0100644)
341 """
339 """
342 if content and commit:
340 if content and commit:
343 raise NodeError("Cannot use both content and commit")
341 raise NodeError("Cannot use both content and commit")
344
342
345 super().__init__(path, kind=NodeKind.FILE)
343 super().__init__(path, kind=NodeKind.FILE)
346
344
347 self.commit = commit
345 self.commit = commit
348 if content and not isinstance(content, bytes):
346 if content and not isinstance(content, bytes):
349 # File content is one thing that inherently must be bytes
347 # File content is one thing that inherently must be bytes
350 # we support passing str too, and convert the content
348 # we support passing str too, and convert the content
351 content = safe_bytes(content)
349 content = safe_bytes(content)
352 self._content = content
350 self._content = content
353 self._mode = mode or FILEMODE_DEFAULT
351 self._mode = mode or FILEMODE_DEFAULT
354
352
355 self._set_bulk_properties(pre_load)
353 self._set_bulk_properties(pre_load)
356
354
357 def __eq__(self, other):
355 def __eq__(self, other):
358 eq = super().__eq__(other)
356 eq = super().__eq__(other)
359 if eq is not None:
357 if eq is not None:
360 return eq
358 return eq
361 return self.content == other.content
359 return self.content == other.content
362
360
363 def __hash__(self):
361 def __hash__(self):
364 raw_id = getattr(self.commit, 'raw_id', '')
362 raw_id = getattr(self.commit, 'raw_id', '')
365 return hash((self.path, raw_id))
363 return hash((self.path, raw_id))
366
364
367 def __lt__(self, other):
365 def __lt__(self, other):
368 lt = super().__lt__(other)
366 lt = super().__lt__(other)
369 if lt is not None:
367 if lt is not None:
370 return lt
368 return lt
371 return self.content < other.content
369 return self.content < other.content
372
370
373 def __repr__(self):
371 def __repr__(self):
374 short_id = getattr(self.commit, 'short_id', '')
372 short_id = getattr(self.commit, 'short_id', '')
375 return f'<{self.__class__.__name__} path={self.path!r}, short_id={short_id}>'
373 return f'<{self.__class__.__name__} path={self.path!r}, short_id={short_id}>'
376
374
377 def _set_bulk_properties(self, pre_load):
375 def _set_bulk_properties(self, pre_load):
378 if not pre_load:
376 if not pre_load:
379 return
377 return
380 pre_load = [entry for entry in pre_load
378 pre_load = [entry for entry in pre_load
381 if entry not in self._filter_pre_load]
379 if entry not in self._filter_pre_load]
382 if not pre_load:
380 if not pre_load:
383 return
381 return
384
382
385 remote = self.commit.get_remote()
383 remote = self.commit.get_remote()
386 result = remote.bulk_file_request(self.commit.raw_id, self.path, pre_load)
384 result = remote.bulk_file_request(self.commit.raw_id, self.path, pre_load)
387
385
388 for attr, value in result.items():
386 for attr, value in result.items():
389 if attr == "flags":
387 if attr == "flags":
390 self.__dict__['mode'] = safe_str(value)
388 self.__dict__['mode'] = safe_str(value)
391 elif attr == "size":
389 elif attr == "size":
392 self.__dict__['size'] = value
390 self.__dict__['size'] = value
393 elif attr == "data":
391 elif attr == "data":
394 self.__dict__['_content'] = value
392 self.__dict__['_content'] = value
395 elif attr == "is_binary":
393 elif attr == "is_binary":
396 self.__dict__['is_binary'] = value
394 self.__dict__['is_binary'] = value
397 elif attr == "md5":
395 elif attr == "md5":
398 self.__dict__['md5'] = value
396 self.__dict__['md5'] = value
399 else:
397 else:
400 raise ValueError(f'Unsupported attr in bulk_property: {attr}')
398 raise ValueError(f'Unsupported attr in bulk_property: {attr}')
401
399
402 @LazyProperty
400 @LazyProperty
403 def mode(self):
401 def mode(self):
404 """
402 """
405 Returns lazily mode of the FileNode. If `commit` is not set, would
403 Returns lazily mode of the FileNode. If `commit` is not set, would
406 use value given at initialization or `FILEMODE_DEFAULT` (default).
404 use value given at initialization or `FILEMODE_DEFAULT` (default).
407 """
405 """
408 if self.commit:
406 if self.commit:
409 mode = self.commit.get_file_mode(self.path)
407 mode = self.commit.get_file_mode(self.path)
410 else:
408 else:
411 mode = self._mode
409 mode = self._mode
412 return mode
410 return mode
413
411
414 @LazyProperty
412 @LazyProperty
415 def raw_bytes(self) -> bytes:
413 def raw_bytes(self) -> bytes:
416 """
414 """
417 Returns lazily the raw bytes of the FileNode.
415 Returns lazily the raw bytes of the FileNode.
418 """
416 """
419 if self.commit:
417 if self.commit:
420 if self._content is None:
418 if self._content is None:
421 self._content = self.commit.get_file_content(self.path)
419 self._content = self.commit.get_file_content(self.path)
422 content = self._content
420 content = self._content
423 else:
421 else:
424 content = self._content
422 content = self._content
425 return content
423 return content
426
424
427 def content_uncached(self):
425 def content_uncached(self):
428 """
426 """
429 Returns lazily content of the FileNode.
427 Returns lazily content of the FileNode.
430 """
428 """
431 if self.commit:
429 if self.commit:
432 content = self.commit.get_file_content(self.path)
430 content = self.commit.get_file_content(self.path)
433 else:
431 else:
434 content = self._content
432 content = self._content
435 return content
433 return content
436
434
437 def stream_bytes(self):
435 def stream_bytes(self):
438 """
436 """
439 Returns an iterator that will stream the content of the file directly from
437 Returns an iterator that will stream the content of the file directly from
440 vcsserver without loading it to memory.
438 vcsserver without loading it to memory.
441 """
439 """
442 if self.commit:
440 if self.commit:
443 return self.commit.get_file_content_streamed(self.path)
441 return self.commit.get_file_content_streamed(self.path)
444 raise NodeError("Cannot retrieve stream_bytes without related commit attribute")
442 raise NodeError("Cannot retrieve stream_bytes without related commit attribute")
445
443
446 def metadata_uncached(self):
444 def metadata_uncached(self):
447 """
445 """
448 Returns md5, binary flag of the file node, without any cache usage.
446 Returns md5, binary flag of the file node, without any cache usage.
449 """
447 """
450
448
451 content = self.content_uncached()
449 content = self.content_uncached()
452
450
453 is_binary = bool(content and BIN_BYTE_MARKER in content)
451 is_binary = bool(content and BIN_BYTE_MARKER in content)
454 size = 0
452 size = 0
455 if content:
453 if content:
456 size = len(content)
454 size = len(content)
457
455
458 return is_binary, md5(content), size, content
456 return is_binary, md5(content), size, content
459
457
460 @LazyProperty
458 @LazyProperty
461 def content(self) -> bytes:
459 def content(self) -> bytes:
462 """
460 """
463 Returns lazily content of the FileNode.
461 Returns lazily content of the FileNode.
464 """
462 """
465 content = self.raw_bytes
463 content = self.raw_bytes
466 if content and not isinstance(content, bytes):
464 if content and not isinstance(content, bytes):
467 raise ValueError(f'Content is of type {type(content)} instead of bytes')
465 raise ValueError(f'Content is of type {type(content)} instead of bytes')
468 return content
466 return content
469
467
470 @LazyProperty
468 @LazyProperty
471 def str_content(self) -> str:
469 def str_content(self) -> str:
472 return safe_str(self.raw_bytes)
470 return safe_str(self.raw_bytes)
473
471
474 @LazyProperty
472 @LazyProperty
475 def size(self):
473 def size(self):
476 if self.commit:
474 if self.commit:
477 return self.commit.get_file_size(self.path)
475 return self.commit.get_file_size(self.path)
478 raise NodeError(
476 raise NodeError(
479 "Cannot retrieve size of the file without related "
477 "Cannot retrieve size of the file without related "
480 "commit attribute")
478 "commit attribute")
481
479
482 @LazyProperty
480 @LazyProperty
483 def message(self):
481 def message(self):
484 if self.commit:
482 if self.commit:
485 return self.last_commit.message
483 return self.last_commit.message
486 raise NodeError(
484 raise NodeError(
487 "Cannot retrieve message of the file without related "
485 "Cannot retrieve message of the file without related "
488 "commit attribute")
486 "commit attribute")
489
487
490 @LazyProperty
488 @LazyProperty
491 def last_commit(self):
489 def last_commit(self):
492 if self.commit:
490 if self.commit:
493 pre_load = ["author", "date", "message", "parents"]
491 pre_load = ["author", "date", "message", "parents"]
494 return self.commit.get_path_commit(self.path, pre_load=pre_load)
492 return self.commit.get_path_commit(self.path, pre_load=pre_load)
495 raise NodeError(
493 raise NodeError(
496 "Cannot retrieve last commit of the file without "
494 "Cannot retrieve last commit of the file without "
497 "related commit attribute")
495 "related commit attribute")
498
496
499 def get_mimetype(self):
497 def get_mimetype(self):
500 """
498 """
501 Mimetype is calculated based on the file's content. If ``_mimetype``
499 Mimetype is calculated based on the file's content. If ``_mimetype``
502 attribute is available, it will be returned (backends which store
500 attribute is available, it will be returned (backends which store
503 mimetypes or can easily recognize them, should set this private
501 mimetypes or can easily recognize them, should set this private
504 attribute to indicate that type should *NOT* be calculated).
502 attribute to indicate that type should *NOT* be calculated).
505 """
503 """
506
504
507 if hasattr(self, '_mimetype'):
505 if hasattr(self, '_mimetype'):
508 if (isinstance(self._mimetype, (tuple, list)) and
506 if (isinstance(self._mimetype, (tuple, list)) and
509 len(self._mimetype) == 2):
507 len(self._mimetype) == 2):
510 return self._mimetype
508 return self._mimetype
511 else:
509 else:
512 raise NodeError('given _mimetype attribute must be an 2 '
510 raise NodeError('given _mimetype attribute must be an 2 '
513 'element list or tuple')
511 'element list or tuple')
514
512
515 db = get_mimetypes_db()
513 db = get_mimetypes_db()
516 mtype, encoding = db.guess_type(self.name)
514 mtype, encoding = db.guess_type(self.name)
517
515
518 if mtype is None:
516 if mtype is None:
519 if not self.is_largefile() and self.is_binary:
517 if not self.is_largefile() and self.is_binary:
520 mtype = 'application/octet-stream'
518 mtype = 'application/octet-stream'
521 encoding = None
519 encoding = None
522 else:
520 else:
523 mtype = 'text/plain'
521 mtype = 'text/plain'
524 encoding = None
522 encoding = None
525
523
526 # try with pygments
524 # try with pygments
527 try:
525 try:
528 from pygments.lexers import get_lexer_for_filename
526 from pygments.lexers import get_lexer_for_filename
529 mt = get_lexer_for_filename(self.name).mimetypes
527 mt = get_lexer_for_filename(self.name).mimetypes
530 except Exception:
528 except Exception:
531 mt = None
529 mt = None
532
530
533 if mt:
531 if mt:
534 mtype = mt[0]
532 mtype = mt[0]
535
533
536 return mtype, encoding
534 return mtype, encoding
537
535
538 @LazyProperty
536 @LazyProperty
539 def mimetype(self):
537 def mimetype(self):
540 """
538 """
541 Wrapper around full mimetype info. It returns only type of fetched
539 Wrapper around full mimetype info. It returns only type of fetched
542 mimetype without the encoding part. use get_mimetype function to fetch
540 mimetype without the encoding part. use get_mimetype function to fetch
543 full set of (type,encoding)
541 full set of (type,encoding)
544 """
542 """
545 return self.get_mimetype()[0]
543 return self.get_mimetype()[0]
546
544
547 @LazyProperty
545 @LazyProperty
548 def mimetype_main(self):
546 def mimetype_main(self):
549 return self.mimetype.split('/')[0]
547 return self.mimetype.split('/')[0]
550
548
551 @classmethod
549 @classmethod
552 def get_lexer(cls, filename, content=None):
550 def get_lexer(cls, filename, content=None):
553 from pygments import lexers
551 from pygments import lexers
554
552
555 extension = filename.split('.')[-1]
553 extension = filename.split('.')[-1]
556 lexer = None
554 lexer = None
557
555
558 try:
556 try:
559 lexer = lexers.guess_lexer_for_filename(
557 lexer = lexers.guess_lexer_for_filename(
560 filename, content, stripnl=False)
558 filename, content, stripnl=False)
561 except lexers.ClassNotFound:
559 except lexers.ClassNotFound:
562 pass
560 pass
563
561
564 # try our EXTENSION_MAP
562 # try our EXTENSION_MAP
565 if not lexer:
563 if not lexer:
566 try:
564 try:
567 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
565 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
568 if lexer_class:
566 if lexer_class:
569 lexer = lexers.get_lexer_by_name(lexer_class[0])
567 lexer = lexers.get_lexer_by_name(lexer_class[0])
570 except lexers.ClassNotFound:
568 except lexers.ClassNotFound:
571 pass
569 pass
572
570
573 if not lexer:
571 if not lexer:
574 lexer = lexers.TextLexer(stripnl=False)
572 lexer = lexers.TextLexer(stripnl=False)
575
573
576 return lexer
574 return lexer
577
575
578 @LazyProperty
576 @LazyProperty
579 def lexer(self):
577 def lexer(self):
580 """
578 """
581 Returns pygment's lexer class. Would try to guess lexer taking file's
579 Returns pygment's lexer class. Would try to guess lexer taking file's
582 content, name and mimetype.
580 content, name and mimetype.
583 """
581 """
584 # TODO: this is more proper, but super heavy on investigating the type based on the content
582 # TODO: this is more proper, but super heavy on investigating the type based on the content
585 #self.get_lexer(self.name, self.content)
583 #self.get_lexer(self.name, self.content)
586
584
587 return self.get_lexer(self.name)
585 return self.get_lexer(self.name)
588
586
589 @LazyProperty
587 @LazyProperty
590 def lexer_alias(self):
588 def lexer_alias(self):
591 """
589 """
592 Returns first alias of the lexer guessed for this file.
590 Returns first alias of the lexer guessed for this file.
593 """
591 """
594 return self.lexer.aliases[0]
592 return self.lexer.aliases[0]
595
593
596 @LazyProperty
594 @LazyProperty
597 def history(self):
595 def history(self):
598 """
596 """
599 Returns a list of commit for this file in which the file was changed
597 Returns a list of commit for this file in which the file was changed
600 """
598 """
601 if self.commit is None:
599 if self.commit is None:
602 raise NodeError('Unable to get commit for this FileNode')
600 raise NodeError('Unable to get commit for this FileNode')
603 return self.commit.get_path_history(self.path)
601 return self.commit.get_path_history(self.path)
604
602
605 @LazyProperty
603 @LazyProperty
606 def annotate(self):
604 def annotate(self):
607 """
605 """
608 Returns a list of three element tuples with lineno, commit and line
606 Returns a list of three element tuples with lineno, commit and line
609 """
607 """
610 if self.commit is None:
608 if self.commit is None:
611 raise NodeError('Unable to get commit for this FileNode')
609 raise NodeError('Unable to get commit for this FileNode')
612 pre_load = ["author", "date", "message", "parents"]
610 pre_load = ["author", "date", "message", "parents"]
613 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
611 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
614
612
615 @LazyProperty
613 @LazyProperty
616 def state(self):
614 def state(self):
617 if not self.commit:
615 if not self.commit:
618 raise NodeError(
616 raise NodeError(
619 "Cannot check state of the node if it's not "
617 "Cannot check state of the node if it's not "
620 "linked with commit")
618 "linked with commit")
621 elif self.path in (node.path for node in self.commit.added):
619 elif self.path in (node.path for node in self.commit.added):
622 return NodeState.ADDED
620 return NodeState.ADDED
623 elif self.path in (node.path for node in self.commit.changed):
621 elif self.path in (node.path for node in self.commit.changed):
624 return NodeState.CHANGED
622 return NodeState.CHANGED
625 else:
623 else:
626 return NodeState.NOT_CHANGED
624 return NodeState.NOT_CHANGED
627
625
628 @LazyProperty
626 @LazyProperty
629 def is_binary(self):
627 def is_binary(self):
630 """
628 """
631 Returns True if file has binary content.
629 Returns True if file has binary content.
632 """
630 """
633 if self.commit:
631 if self.commit:
634 return self.commit.is_node_binary(self.path)
632 return self.commit.is_node_binary(self.path)
635 else:
633 else:
636 raw_bytes = self._content
634 raw_bytes = self._content
637 return bool(raw_bytes and BIN_BYTE_MARKER in raw_bytes)
635 return bool(raw_bytes and BIN_BYTE_MARKER in raw_bytes)
638
636
639 @LazyProperty
637 @LazyProperty
640 def md5(self):
638 def md5(self):
641 """
639 """
642 Returns md5 of the file node.
640 Returns md5 of the file node.
643 """
641 """
644
642
645 if self.commit:
643 if self.commit:
646 return self.commit.node_md5_hash(self.path)
644 return self.commit.node_md5_hash(self.path)
647 else:
645 else:
648 raw_bytes = self._content
646 raw_bytes = self._content
649 # TODO: this sucks, we're computing md5 on potentially super big stream data...
647 # TODO: this sucks, we're computing md5 on potentially super big stream data...
650 return md5(raw_bytes)
648 return md5(raw_bytes)
651
649
652 @LazyProperty
650 @LazyProperty
653 def extension(self):
651 def extension(self):
654 """Returns filenode extension"""
652 """Returns filenode extension"""
655 return self.name.split('.')[-1]
653 return self.name.split('.')[-1]
656
654
657 @property
655 @property
658 def is_executable(self):
656 def is_executable(self):
659 """
657 """
660 Returns ``True`` if file has executable flag turned on.
658 Returns ``True`` if file has executable flag turned on.
661 """
659 """
662 return bool(self.mode & stat.S_IXUSR)
660 return bool(self.mode & stat.S_IXUSR)
663
661
664 def get_largefile_node(self):
662 def get_largefile_node(self):
665 """
663 """
666 Try to return a Mercurial FileNode from this node. It does internal
664 Try to return a Mercurial FileNode from this node. It does internal
667 checks inside largefile store, if that file exist there it will
665 checks inside largefile store, if that file exist there it will
668 create special instance of LargeFileNode which can get content from
666 create special instance of LargeFileNode which can get content from
669 LF store.
667 LF store.
670 """
668 """
671 if self.commit:
669 if self.commit:
672 return self.commit.get_largefile_node(self.path)
670 return self.commit.get_largefile_node(self.path)
673
671
674 def count_lines(self, content: str | bytes, count_empty=False):
672 def count_lines(self, content: str | bytes, count_empty=False):
675 if isinstance(content, str):
673 if isinstance(content, str):
676 newline_marker = '\n'
674 newline_marker = '\n'
677 elif isinstance(content, bytes):
675 elif isinstance(content, bytes):
678 newline_marker = b'\n'
676 newline_marker = b'\n'
679 else:
677 else:
680 raise ValueError('content must be bytes or str got {type(content)} instead')
678 raise ValueError('content must be bytes or str got {type(content)} instead')
681
679
682 if count_empty:
680 if count_empty:
683 all_lines = 0
681 all_lines = 0
684 empty_lines = 0
682 empty_lines = 0
685 for line in content.splitlines(True):
683 for line in content.splitlines(True):
686 if line == newline_marker:
684 if line == newline_marker:
687 empty_lines += 1
685 empty_lines += 1
688 all_lines += 1
686 all_lines += 1
689
687
690 return all_lines, all_lines - empty_lines
688 return all_lines, all_lines - empty_lines
691 else:
689 else:
692 # fast method
690 # fast method
693 empty_lines = all_lines = content.count(newline_marker)
691 empty_lines = all_lines = content.count(newline_marker)
694 if all_lines == 0 and content:
692 if all_lines == 0 and content:
695 # one-line without a newline
693 # one-line without a newline
696 empty_lines = all_lines = 1
694 empty_lines = all_lines = 1
697
695
698 return all_lines, empty_lines
696 return all_lines, empty_lines
699
697
700 def lines(self, count_empty=False):
698 def lines(self, count_empty=False):
701 all_lines, empty_lines = 0, 0
699 all_lines, empty_lines = 0, 0
702
700
703 if not self.is_binary:
701 if not self.is_binary:
704 content = self.content
702 content = self.content
705 all_lines, empty_lines = self.count_lines(content, count_empty=count_empty)
703 all_lines, empty_lines = self.count_lines(content, count_empty=count_empty)
706 return all_lines, empty_lines
704 return all_lines, empty_lines
707
705
708
706
709 class RemovedFileNode(FileNode):
707 class RemovedFileNode(FileNode):
710 """
708 """
711 Dummy FileNode class - trying to access any public attribute except path,
709 Dummy FileNode class - trying to access any public attribute except path,
712 name, kind or state (or methods/attributes checking those two) would raise
710 name, kind or state (or methods/attributes checking those two) would raise
713 RemovedFileNodeError.
711 RemovedFileNodeError.
714 """
712 """
715 ALLOWED_ATTRIBUTES = [
713 ALLOWED_ATTRIBUTES = [
716 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
714 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
717 'added', 'changed', 'not_changed', 'removed', 'bytes_path'
715 'added', 'changed', 'not_changed', 'removed', 'bytes_path'
718 ]
716 ]
719
717
720 def __init__(self, path):
718 def __init__(self, path):
721 """
719 """
722 :param path: relative path to the node
720 :param path: relative path to the node
723 """
721 """
724 super().__init__(path=path)
722 super().__init__(path=path)
725
723
726 def __getattribute__(self, attr):
724 def __getattribute__(self, attr):
727 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
725 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
728 return super().__getattribute__(attr)
726 return super().__getattribute__(attr)
729 raise RemovedFileNodeError(f"Cannot access attribute {attr} on RemovedFileNode. Not in allowed attributes")
727 raise RemovedFileNodeError(f"Cannot access attribute {attr} on RemovedFileNode. Not in allowed attributes")
730
728
731 @LazyProperty
729 @LazyProperty
732 def state(self):
730 def state(self):
733 return NodeState.REMOVED
731 return NodeState.REMOVED
734
732
735
733
736 class DirNode(Node):
734 class DirNode(Node):
737 """
735 """
738 DirNode stores list of files and directories within this node.
736 DirNode stores list of files and directories within this node.
739 Nodes may be used standalone but within repository context they
737 Nodes may be used standalone but within repository context they
740 lazily fetch data within same repository's commit.
738 lazily fetch data within same repository's commit.
741 """
739 """
742
740
743 def __init__(self, path, nodes=(), commit=None, default_pre_load=None):
741 def __init__(self, path, nodes=(), commit=None, default_pre_load=None):
744 """
742 """
745 Only one of ``nodes`` and ``commit`` may be given. Passing both
743 Only one of ``nodes`` and ``commit`` may be given. Passing both
746 would raise ``NodeError`` exception.
744 would raise ``NodeError`` exception.
747
745
748 :param path: relative path to the node
746 :param path: relative path to the node
749 :param nodes: content may be passed to constructor
747 :param nodes: content may be passed to constructor
750 :param commit: if given, will use it to lazily fetch content
748 :param commit: if given, will use it to lazily fetch content
751 """
749 """
752 if nodes and commit:
750 if nodes and commit:
753 raise NodeError("Cannot use both nodes and commit")
751 raise NodeError("Cannot use both nodes and commit")
754 super().__init__(path, NodeKind.DIR)
752 super().__init__(path, NodeKind.DIR)
755 self.commit = commit
753 self.commit = commit
756 self._nodes = nodes
754 self._nodes = nodes
757 self.default_pre_load = default_pre_load or ['is_binary', 'size']
755 self.default_pre_load = default_pre_load or ['is_binary', 'size']
758
756
759 def __iter__(self):
757 def __iter__(self):
760 yield from self.nodes
758 yield from self.nodes
761
759
762 def __eq__(self, other):
760 def __eq__(self, other):
763 eq = super().__eq__(other)
761 eq = super().__eq__(other)
764 if eq is not None:
762 if eq is not None:
765 return eq
763 return eq
766 # check without entering each dir
764 # check without entering each dir
767 self_nodes_paths = list(sorted(n.path for n in self.nodes))
765 self_nodes_paths = list(sorted(n.path for n in self.nodes))
768 other_nodes_paths = list(sorted(n.path for n in self.nodes))
766 other_nodes_paths = list(sorted(n.path for n in self.nodes))
769 return self_nodes_paths == other_nodes_paths
767 return self_nodes_paths == other_nodes_paths
770
768
771 def __lt__(self, other):
769 def __lt__(self, other):
772 lt = super().__lt__(other)
770 lt = super().__lt__(other)
773 if lt is not None:
771 if lt is not None:
774 return lt
772 return lt
775 # check without entering each dir
773 # check without entering each dir
776 self_nodes_paths = list(sorted(n.path for n in self.nodes))
774 self_nodes_paths = list(sorted(n.path for n in self.nodes))
777 other_nodes_paths = list(sorted(n.path for n in self.nodes))
775 other_nodes_paths = list(sorted(n.path for n in self.nodes))
778 return self_nodes_paths < other_nodes_paths
776 return self_nodes_paths < other_nodes_paths
779
777
780 @LazyProperty
778 @LazyProperty
781 def content(self):
779 def content(self):
782 raise NodeError(f"{self} represents a dir and has no `content` attribute")
780 raise NodeError(f"{self} represents a dir and has no `content` attribute")
783
781
784 @LazyProperty
782 @LazyProperty
785 def nodes(self):
783 def nodes(self):
786 if self.commit:
784 if self.commit:
787 nodes = self.commit.get_nodes(self.path, pre_load=self.default_pre_load)
785 nodes = self.commit.get_nodes(self.path, pre_load=self.default_pre_load)
788 else:
786 else:
789 nodes = self._nodes
787 nodes = self._nodes
790 self._nodes_dict = {node.path: node for node in nodes}
788 self._nodes_dict = {node.path: node for node in nodes}
791 return sorted(nodes)
789 return sorted(nodes)
792
790
793 @LazyProperty
791 @LazyProperty
794 def files(self):
792 def files(self):
795 return sorted(node for node in self.nodes if node.is_file())
793 return sorted(node for node in self.nodes if node.is_file())
796
794
797 @LazyProperty
795 @LazyProperty
798 def dirs(self):
796 def dirs(self):
799 return sorted(node for node in self.nodes if node.is_dir())
797 return sorted(node for node in self.nodes if node.is_dir())
800
798
801 def get_node(self, path):
799 def get_node(self, path):
802 """
800 """
803 Returns node from within this particular ``DirNode``, so it is now
801 Returns node from within this particular ``DirNode``, so it is now
804 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
802 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
805 'docs'. In order to access deeper nodes one must fetch nodes between
803 'docs'. In order to access deeper nodes one must fetch nodes between
806 them first - this would work::
804 them first - this would work::
807
805
808 docs = root.get_node('docs')
806 docs = root.get_node('docs')
809 docs.get_node('api').get_node('index.rst')
807 docs.get_node('api').get_node('index.rst')
810
808
811 :param: path - relative to the current node
809 :param: path - relative to the current node
812
810
813 .. note::
811 .. note::
814 To access lazily (as in example above) node have to be initialized
812 To access lazily (as in example above) node have to be initialized
815 with related commit object - without it node is out of
813 with related commit object - without it node is out of
816 context and may know nothing about anything else than nearest
814 context and may know nothing about anything else than nearest
817 (located at same level) nodes.
815 (located at same level) nodes.
818 """
816 """
819 try:
817 try:
820 path = path.rstrip('/')
818 path = path.rstrip('/')
821 if path == '':
819 if path == '':
822 raise NodeError("Cannot retrieve node without path")
820 raise NodeError("Cannot retrieve node without path")
823 self.nodes # access nodes first in order to set _nodes_dict
821 self.nodes # access nodes first in order to set _nodes_dict
824 paths = path.split('/')
822 paths = path.split('/')
825 if len(paths) == 1:
823 if len(paths) == 1:
826 if not self.is_root():
824 if not self.is_root():
827 path = '/'.join((self.path, paths[0]))
825 path = '/'.join((self.path, paths[0]))
828 else:
826 else:
829 path = paths[0]
827 path = paths[0]
830 return self._nodes_dict[path]
828 return self._nodes_dict[path]
831 elif len(paths) > 1:
829 elif len(paths) > 1:
832 if self.commit is None:
830 if self.commit is None:
833 raise NodeError("Cannot access deeper nodes without commit")
831 raise NodeError("Cannot access deeper nodes without commit")
834 else:
832 else:
835 path1, path2 = paths[0], '/'.join(paths[1:])
833 path1, path2 = paths[0], '/'.join(paths[1:])
836 return self.get_node(path1).get_node(path2)
834 return self.get_node(path1).get_node(path2)
837 else:
835 else:
838 raise KeyError
836 raise KeyError
839 except KeyError:
837 except KeyError:
840 raise NodeError(f"Node does not exist at {path}")
838 raise NodeError(f"Node does not exist at {path}")
841
839
842 @LazyProperty
840 @LazyProperty
843 def state(self):
841 def state(self):
844 raise NodeError("Cannot access state of DirNode")
842 raise NodeError("Cannot access state of DirNode")
845
843
846 @LazyProperty
844 @LazyProperty
847 def size(self):
845 def size(self):
848 size = 0
846 size = 0
849 for root, dirs, files in self.commit.walk(self.path):
847 for root, dirs, files in self.commit.walk(self.path):
850 for f in files:
848 for f in files:
851 size += f.size
849 size += f.size
852
850
853 return size
851 return size
854
852
855 @LazyProperty
853 @LazyProperty
856 def last_commit(self):
854 def last_commit(self):
857 if self.commit:
855 if self.commit:
858 pre_load = ["author", "date", "message", "parents"]
856 pre_load = ["author", "date", "message", "parents"]
859 return self.commit.get_path_commit(self.path, pre_load=pre_load)
857 return self.commit.get_path_commit(self.path, pre_load=pre_load)
860 raise NodeError(
858 raise NodeError(
861 "Cannot retrieve last commit of the file without "
859 "Cannot retrieve last commit of the file without "
862 "related commit attribute")
860 "related commit attribute")
863
861
864 def __repr__(self):
862 def __repr__(self):
865 short_id = getattr(self.commit, 'short_id', '')
863 short_id = getattr(self.commit, 'short_id', '')
866 return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>'
864 return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>'
867
865
868
866
869 class RootNode(DirNode):
867 class RootNode(DirNode):
870 """
868 """
871 DirNode being the root node of the repository.
869 DirNode being the root node of the repository.
872 """
870 """
873
871
874 def __init__(self, nodes=(), commit=None):
872 def __init__(self, nodes=(), commit=None):
875 super().__init__(path=b'', nodes=nodes, commit=commit)
873 super().__init__(path=b'', nodes=nodes, commit=commit)
876
874
877 def __repr__(self):
875 def __repr__(self):
878 return f'<{self.__class__.__name__}>'
876 return f'<{self.__class__.__name__}>'
879
877
880
878
881 class SubModuleNode(Node):
879 class SubModuleNode(Node):
882 """
880 """
883 represents a SubModule of Git or SubRepo of Mercurial
881 represents a SubModule of Git or SubRepo of Mercurial
884 """
882 """
885 is_binary = False
883 is_binary = False
886 size = 0
884 size = 0
887
885
888 def __init__(self, name, url=None, commit=None, alias=None):
886 def __init__(self, name, url=None, commit=None, alias=None):
889 self.path = name
887 self.path = name
890 self.kind = NodeKind.SUBMODULE
888 self.kind = NodeKind.SUBMODULE
891 self.alias = alias
889 self.alias = alias
892
890
893 # we have to use EmptyCommit here since this can point to svn/git/hg
891 # we have to use EmptyCommit here since this can point to svn/git/hg
894 # submodules we cannot get from repository
892 # submodules we cannot get from repository
895 self.commit = EmptyCommit(str(commit), alias=alias)
893 self.commit = EmptyCommit(str(commit), alias=alias)
896 self.url = url or self._extract_submodule_url()
894 self.url = url or self._extract_submodule_url()
897
895
898 def __repr__(self):
896 def __repr__(self):
899 short_id = getattr(self.commit, 'short_id', '')
897 short_id = getattr(self.commit, 'short_id', '')
900 return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>'
898 return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>'
901
899
902 def _extract_submodule_url(self):
900 def _extract_submodule_url(self):
903 # TODO: find a way to parse gits submodule file and extract the
901 # TODO: find a way to parse gits submodule file and extract the
904 # linking URL
902 # linking URL
905 return self.path
903 return self.path
906
904
907 @LazyProperty
905 @LazyProperty
908 def name(self):
906 def name(self):
909 """
907 """
910 Returns name of the node so if its path
908 Returns name of the node so if its path
911 then only last part is returned.
909 then only last part is returned.
912 """
910 """
913 org = safe_str(self.path.rstrip('/').split('/')[-1])
911 org = safe_str(self.path.rstrip('/').split('/')[-1])
914 return f'{org} @ {self.commit.short_id}'
912 return f'{org} @ {self.commit.short_id}'
915
913
916
914
917 class LargeFileNode(FileNode):
915 class LargeFileNode(FileNode):
918
916
919 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
917 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
920 self._validate_path(path) # can throw exception if path is invalid
918 self._validate_path(path) # can throw exception if path is invalid
921 self.org_path = org_path # as stored in VCS as LF pointer
919 self.org_path = org_path # as stored in VCS as LF pointer
922
920
923 self.bytes_path = path.rstrip(b'/') # store for __repr__
921 self.bytes_path = path.rstrip(b'/') # store for __repr__
924 self.path = safe_str(self.bytes_path) # we store paths as str
922 self.path = safe_str(self.bytes_path) # we store paths as str
925
923
926 self.kind = NodeKind.LARGEFILE
924 self.kind = NodeKind.LARGEFILE
927 self.alias = alias
925 self.alias = alias
928 self._content = b''
926 self._content = b''
929
927
930 def _validate_path(self, path: bytes):
928 def _validate_path(self, path: bytes):
931 """
929 """
932 we override check since the LargeFileNode path is system absolute, but we check for bytes only
930 we override check since the LargeFileNode path is system absolute, but we check for bytes only
933 """
931 """
934 self._assert_bytes(path)
932 self._assert_bytes(path)
935
933
936 def __repr__(self):
934 def __repr__(self):
937 return f'<{self.__class__.__name__} {self.org_path} -> {self.path!r}>'
935 return f'<{self.__class__.__name__} {self.org_path} -> {self.path!r}>'
938
936
939 @LazyProperty
937 @LazyProperty
940 def size(self):
938 def size(self):
941 return os.stat(self.path).st_size
939 return os.stat(self.path).st_size
942
940
943 @LazyProperty
941 @LazyProperty
944 def raw_bytes(self):
942 def raw_bytes(self):
945 with open(self.path, 'rb') as f:
943 with open(self.path, 'rb') as f:
946 content = f.read()
944 content = f.read()
947 return content
945 return content
948
946
949 @LazyProperty
947 @LazyProperty
950 def name(self):
948 def name(self):
951 """
949 """
952 Overwrites name to be the org lf path
950 Overwrites name to be the org lf path
953 """
951 """
954 return self.org_path
952 return self.org_path
955
953
956 def stream_bytes(self):
954 def stream_bytes(self):
957 with open(self.path, 'rb') as stream:
955 with open(self.path, 'rb') as stream:
958 while True:
956 while True:
959 data = stream.read(16 * 1024)
957 data = stream.read(16 * 1024)
960 if not data:
958 if not data:
961 break
959 break
962 yield data
960 yield data
General Comments 0
You need to be logged in to leave comments. Login now