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